Skip to content

feat: scheduled pipelines + dependency cascade fix#80

Merged
dean0x merged 17 commits intomainfrom
feat/scheduled-pipelines-78
Mar 12, 2026
Merged

feat: scheduled pipelines + dependency cascade fix#80
dean0x merged 17 commits intomainfrom
feat/scheduled-pipelines-78

Conversation

@dean0x
Copy link
Owner

@dean0x dean0x commented Mar 11, 2026

Summary

  • Scheduled Pipelines: New SchedulePipeline MCP tool and --pipeline --step CLI flags for creating cron or one-time schedules that trigger multi-step pipelines (2–20 steps). Each trigger creates fresh tasks with linear dependencies.
  • Dependency Failure Cascade (breaking): Failed/cancelled upstream tasks now cascade cancellation to dependents instead of incorrectly unblocking them.
  • Queue Handler Race Condition: Fast-path dependencyState check prevents blocked tasks from being enqueued before dependency rows are written to DB.

Changes (21 files, +2180 / -183)

Bug fixes:

  • dependency-handler.ts — cascade cancellation on failed/cancelled upstream
  • queue-handler.ts — fast-path blocked task check via dependencyState

Core:

  • domain.tspipelineSteps on Schedule, ScheduledPipelineCreateRequest
  • interfaces.tscreateScheduledPipeline(), cancelTasks on cancel
  • database.ts — migration 8: pipeline_steps, pipeline_task_ids columns
  • schedule-repository.ts — JSON round-trip with Zod validation
  • schedule-manager.tscreateScheduledPipeline(), shared timing validation, cancelTasks support
  • schedule-handler.tshandlePipelineTrigger() with partial save failure cleanup

Adapters & CLI:

  • mcp-adapter.tsSchedulePipeline tool, enhanced CancelSchedule/ListSchedules/GetSchedule
  • schedule.ts--pipeline --step flags, --cancel-tasks on cancel

Tests (188 new, 1,467 total):

  • Dependency cascade (3), queue handler fast-path (2), schedule handler pipeline (5)
  • Schedule manager (5), schedule repo (4), MCP adapter (5), CLI (3)

Docs:

  • CLAUDE.md, FEATURES.md, ROADMAP.md, README.md updated for v0.6.0

Closes #78

Test plan

  • npm run test:handlers — 115 passed (dependency cascade + queue handler + schedule handler pipeline)
  • npm run test:services — 141 passed (schedule manager createScheduledPipeline)
  • npm run test:implementations — 302 passed (schedule repo pipeline round-trip)
  • npm run test:adapters — 55 passed (SchedulePipeline tool, CancelSchedule cancelTasks)
  • npm run test:cli — 150 passed (--pipeline --step, --cancel-tasks)
  • npm run test:all — 1,467 tests, zero regressions
  • npm run build — clean

Dean Sharon added 2 commits March 11, 2026 16:06
Add SchedulePipeline MCP tool and CLI support for creating recurring or
one-time schedules that trigger multi-step pipelines. Each trigger creates
fresh tasks with linear dependencies (step N depends on step N-1).

Bug fixes:
- Dependency failure cascade: failed/cancelled upstream tasks now cascade
  cancellation to dependents instead of incorrectly unblocking them
- Queue handler race condition: fast-path dependencyState check prevents
  blocked tasks from being enqueued before dependency rows are written

Features:
- SchedulePipeline MCP tool (2-20 steps, cron/one-time, per-step agent)
- CLI: --pipeline --step flags for schedule create
- CancelSchedule: optional cancelTasks flag for in-flight pipeline tasks
- ListSchedules: isPipeline/stepCount indicators
- GetSchedule: pipelineSteps in response
- Database migration 8: pipeline_steps, pipeline_task_ids columns
- 188 new tests (1,467 total), zero regressions
@greptile-apps
Copy link

greptile-apps bot commented Mar 11, 2026

Confidence Score: 3/5

  • Safe to merge with awareness of the pipeline-task orphaning risk under transient DB errors on CRON schedules
  • The bug and queue-handler fixes are correct and well-tested. The pipeline scheduling feature works correctly in the happy path and several previously flagged issues were addressed. However, a failure in updateScheduleAfterTrigger within handlePipelineTrigger leaves up to 20 orphaned tasks per trigger with no cleanup, and for CRON schedules this silently repeats on every tick — a data-accumulation hazard that warrants resolution before wide deployment. The missing log in rowToExecution's catch block is a lower-severity observability gap.
  • src/services/handlers/schedule-handler.ts (orphaned pipeline tasks on updateScheduleAfterTrigger failure) and src/implementations/schedule-repository.ts (silent error swallow in rowToExecution)

Important Files Changed

Filename Overview
src/services/handlers/schedule-handler.ts Introduces handlePipelineTrigger and extracts shared helpers (resolveAfterScheduleTaskId, recordFailedExecution, recordTriggeredExecution, updateScheduleAfterTrigger). Step-0 TaskDelegated failure now correctly cancels all tasks and returns an error rather than silently succeeding. However, updateScheduleAfterTrigger failure (lines 412–414) leaves all N saved pipeline tasks permanently orphaned with no cleanup, and for CRON schedules re-triggers the same failure in a loop.
src/services/handlers/dependency-handler.ts Adds cascade-cancellation logic: when a dependency resolves as failed/cancelled, downstream tasks now receive a TaskCancellationRequested event instead of being unblocked. getDependencies failure is handled conservatively (skip unblock + warn log) as flagged in previous review.
src/services/handlers/queue-handler.ts Adds a fast-path early-return when event.task.dependencyState === 'blocked', eliminating the race window where isBlocked() returns false before DependencyHandler has written dependency rows. Clean, minimal change.
src/services/schedule-manager.ts Adds createScheduledPipeline, extracts validateScheduleTiming shared helper, and adds cancelTasks support to cancelSchedule. Per-step path normalization issue (previously flagged) was addressed by building normalizedSteps in-place. cancelTasks still only covers the latest execution (1-window); previously flagged.
src/implementations/schedule-repository.ts Adds JSON round-trip with Zod validation for new pipeline_steps and pipeline_task_ids columns. rowToExecution silently swallows pipelineTaskIds parse errors without logging (comment says "log but don't fail" — no logger exists in this class). rowToSchedule correctly throws on corrupt pipeline_steps.
src/adapters/mcp-adapter.ts Adds SchedulePipeline tool with well-structured Zod schema, enhances CancelSchedule with cancelTasks flag, and enriches ListSchedules/GetSchedule responses with isPipeline/stepCount/pipelineSteps. Response field cancelTasksRequested clearly communicates it's a boolean echo of the input flag (previously flagged field name resolved).
src/cli/commands/schedule.ts Adds --pipeline/--step flags and --cancel-tasks for cancel subcommand. Both the "positional prompt words in pipeline mode" (emits an info message instead of hard error) and the "--step without --pipeline" guard are handled as previously flagged. toMissedRunPolicy refactoring reduces duplication.
src/core/domain.ts Adds pipelineSteps to Schedule/ScheduleRequest and new ScheduledPipelineCreateRequest interface. Clean, minimal extension that reuses the existing PipelineStepRequest type.
src/implementations/database.ts Migration 8 adds nullable pipeline_steps TEXT to schedules and pipeline_task_ids TEXT to schedule_executions. Additive-only migration with no breaking schema changes.

Sequence Diagram

sequenceDiagram
    participant Scheduler
    participant ScheduleHandler
    participant TaskRepo
    participant ScheduleRepo
    participant EventBus
    participant DependencyHandler
    participant QueueHandler

    Scheduler->>ScheduleHandler: ScheduleTriggered (pipeline schedule)
    ScheduleHandler->>ScheduleHandler: resolveAfterScheduleTaskId()

    loop For each step i
        ScheduleHandler->>TaskRepo: save(task_i, dependsOn=[task_{i-1}])
        TaskRepo-->>ScheduleHandler: ok(task_i)
    end

    ScheduleHandler->>ScheduleRepo: recordTriggeredExecution(lastTaskId, allTaskIds)
    ScheduleHandler->>ScheduleRepo: updateScheduleAfterTrigger()
    Note over ScheduleHandler: ⚠️ If update fails here, N tasks orphaned with no cleanup

    loop Emit TaskDelegated per step
        ScheduleHandler->>EventBus: emit TaskDelegated(task_i)
        Note over ScheduleHandler: Step 0 failure → cancel all tasks + return error
        EventBus->>QueueHandler: TaskDelegated(task_0)
        QueueHandler->>QueueHandler: dependencyState==='blocked'? fast-path skip
        QueueHandler->>EventBus: emit TaskQueued(task_0)
    end

    ScheduleHandler->>EventBus: emit ScheduleExecuted(lastTaskId)

    Note over EventBus,DependencyHandler: When task_0 completes...
    EventBus->>DependencyHandler: TaskCompleted(task_0)
    DependencyHandler->>DependencyHandler: getDependencies(task_1) — check for failed/cancelled deps
    alt All deps completed
        DependencyHandler->>EventBus: emit TaskUnblocked(task_1)
    else Any dep failed/cancelled
        DependencyHandler->>EventBus: emit TaskCancellationRequested(task_1)
    end
Loading

Last reviewed commit: bd56703

this.logger.info('Injected afterSchedule dependency on pipeline step 0', {
scheduleId,
afterScheduleId: schedule.afterScheduleId,
dependsOnTaskId: latestExecution.taskId,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: Duplicated afterScheduleId resolution logic

The handlePipelineTrigger method contains inline afterScheduleId resolution (lines 327-345 approximately) that duplicates the extracted resolveAfterScheduleDependency helper (lines 455-483). The single-task path correctly calls the shared helper, but the pipeline path re-implements the same business logic:

  • Fetch execution history
  • Get latest execution
  • Check if task exists and is in terminal state
  • Return undefined or the task ID

Impact: Two places to maintain the same logic. If afterScheduleId resolution rules change (e.g., checking multiple executions, different terminal state semantics), both paths must be updated independently.

Fix: Refactor resolveAfterScheduleDependency to return the resolved TaskId | undefined instead of a modified template, so both handleSingleTaskTrigger and handlePipelineTrigger consume the same primitive:

private async resolveAfterScheduleTaskId(afterScheduleId: ScheduleId): Promise<TaskId | undefined> {
  const historyResult = await this.scheduleRepo.getExecutionHistory(afterScheduleId, 1);
  if (!historyResult.ok || historyResult.value.length === 0) return undefined;
  const latestExecution = historyResult.value[0];
  if (!latestExecution.taskId) return undefined;
  const depTaskResult = await this.taskRepo.findById(latestExecution.taskId);
  if (!depTaskResult.ok || !depTaskResult.value || isTerminalState(depTaskResult.value.status)) {
    return undefined;
  }
  return latestExecution.taskId;
}

Flagged by: Architecture, Complexity, Consistency reviews

@dean0x
Copy link
Owner Author

dean0x commented Mar 11, 2026

Code Review Summary: PR #80 - Scheduled Pipelines

Status: CHANGES_REQUESTED

This PR introduces scheduled pipelines with dependency failure cascade fixes. The feature is well-structured overall, but there are 8 HIGH severity and 5 MEDIUM severity blocking issues that should be addressed before merge. Below is a deduplicated summary based on comprehensive reviews from 10 reviewers (Architecture, Complexity, Consistency, Database, Documentation, Performance, Regression, Security, Tests, TypeScript).


BLOCKING ISSUES

Critical

1. TASK-DEPENDENCIES.md contradicts cascade behavior (docs/TASK-DEPENDENCIES.md:468-524)

  • Status: Documentation explicitly contradicts code implementation
  • Impact: Users relying on documented behavior (failed deps unblock downstream tasks) will be surprised by automatic cancellation
  • Fix: Update documentation sections:
    • "Handle Dependency Failures" (line 466+): Replace v0.3.0 behavior with v0.6.0 cascade semantics
    • "Error Handling" example (line 377-403): Update to show cascade cancellation
    • "Cancelled Dependency Propagation" (line 554-585): Remove "does NOT automatically cancel dependents" claim
    • "Design Rationale" (line 526-538): Update since behavior changed
    • "Future Consideration (v0.4.0)" (line 539-552): Mark as implemented

HIGH Issues

2. Duplicated afterScheduleId resolution logic (src/services/handlers/schedule-handler.ts:327-345)

  • Files: schedule-handler.ts:327-345 (pipeline path) vs schedule-handler.ts:455-483 (single-task helper)
  • Reviewers: Architecture, Complexity, Consistency
  • Status: CONFIRMED - Multiple reviewers flagged the same duplication
  • Impact: Two places to maintain same business logic; divergence risk if rules change
  • Fix: Refactor resolveAfterScheduleDependency to return TaskId | undefined instead of modified template
private async resolveAfterScheduleTaskId(afterScheduleId: ScheduleId): Promise<TaskId | undefined> {
  const historyResult = await this.scheduleRepo.getExecutionHistory(afterScheduleId, 1);
  if (!historyResult.ok || historyResult.value.length === 0) return undefined;
  const latestExecution = historyResult.value[0];
  if (!latestExecution.taskId) return undefined;
  const depTaskResult = await this.taskRepo.findById(latestExecution.taskId);
  if (!depTaskResult.ok || !depTaskResult.value || isTerminalState(depTaskResult.value.status)) return undefined;
  return latestExecution.taskId;
}

3. createSchedule not using validateScheduleTiming (src/services/schedule-manager.ts:64-155 vs lines 491+)

  • Files: schedule-manager.ts
  • Reviewers: Architecture, Complexity, Consistency
  • Status: CONFIRMED - Multiple reviewers flagged the same duplication
  • Impact: ~80 lines of validation logic duplicated; JSDoc claims helper is "shared between createSchedule and createScheduledPipeline" but createSchedule was never refactored
  • Fix: Call validateScheduleTiming from createSchedule to consolidate validation logic

4. Pipeline task creation loop not wrapped in transaction (src/services/handlers/schedule-handler.ts:340-399)

  • File: schedule-handler.ts
  • Reviewer: Database
  • Impact: Process crash mid-loop leaves orphaned tasks and unresolved dependency rows; partial pipeline state with no recovery mechanism
  • Fix: Wrap entire task creation loop in a database transaction:
const txResult = await this.taskRepo.transaction(async (txRepo) => {
  for (let i = 0; i < steps.length; i++) {
    // ... build task ...
    const saveResult = await txRepo.save(task);
    if (!saveResult.ok) return saveResult;
    savedTasks.push(task);
  }
  return ok(undefined);
});
if (!txResult.ok) {
  await this.recordFailedExecution(...);
  return txResult;
}

5. MCP adapter tests use simulate helpers instead of real code (tests/unit/adapters/mcp-adapter.test.ts:857-990)

  • File: mcp-adapter.test.ts
  • Reviewer: Tests
  • Impact: ZERO integration coverage of new adapter code paths (handleSchedulePipeline, enhanced handleCancelSchedule, handleListSchedules, handleGetSchedule)
  • Fix: Replace helper calls with actual adapter.handleToolCall('SchedulePipeline', ...) to exercise real adapter methods

6. Missing release notes for v0.6.0 (docs/releases/)

  • File: docs/releases/
  • Reviewer: Documentation
  • Impact: Release workflow will fail validation since release notes file is required
  • Fix: Create docs/releases/RELEASE_NOTES_v0.6.0.md with: SchedulePipeline MCP tool, CLI flags, dependency cascade fix, queue handler race condition fix, cancelTasks, migration 8 schema changes, breaking change note

7. No JSDoc on cancelSchedule updated signature (src/services/schedule-manager.ts:238, src/core/interfaces.ts:408)

  • Files: schedule-manager.ts, interfaces.ts
  • Reviewer: Documentation
  • Impact: Third parameter cancelTasks is undocumented; callers don't know behavior
  • Fix: Add JSDoc documenting the new parameter
/**
 * @param cancelTasks - If true, also cancel in-flight tasks from the latest execution
 */

8. No JSDoc on createScheduledPipeline (src/services/schedule-manager.ts:325, src/core/interfaces.ts:410)

  • Files: schedule-manager.ts, interfaces.ts
  • Reviewer: Documentation
  • Impact: New public API undocumented; no mention of validation constraints (2-20 steps, path validation, agent resolution), error conditions
  • Fix: Add comprehensive JSDoc covering behavior, constraints, errors

MEDIUM Issues

9. Pipeline cleanup bypasses event system (src/services/handlers/schedule-handler.ts:380-387)

  • File: schedule-handler.ts
  • Reviewer: Architecture
  • Impact: Direct DB mutation skips dependency resolution and audit logging; orphaned dependency rows remain in pending state
  • Fix: Either wrap task creation in transaction (fixes this too) OR emit TaskCancellationRequested events and document architectural exception

10. Validation duplication in CLI (src/cli/commands/schedule.ts:172-175 and 222-228)

  • File: cli/commands/schedule.ts
  • Reviewer: Complexity
  • Impact: Duplicated missedRunPolicy ternary chain within same function
  • Fix: Extract to helper or reuse existing toMissedRunPolicy from schedule-manager.ts

11. Non-null assertion on pipelineSteps (src/services/handlers/schedule-handler.ts:319)

  • File: schedule-handler.ts
  • Reviewer: TypeScript
  • Impact: const steps = schedule.pipelineSteps!; bypasses type narrowing; no compile-time safety if called from different path
  • Fix: Accept steps as parameter or add local type guard
// Option A: Accept as parameter
private async handlePipelineTrigger(
  schedule: Schedule,
  triggeredAt: number,
  steps: readonly PipelineStepRequest[]
): Promise<Result<void>> { ... }

// Option B: Local guard
if (!schedule.pipelineSteps || schedule.pipelineSteps.length === 0) {
  return err(new BackbeatError(ErrorCode.INVALID_INPUT, 'Pipeline requires steps'));
}
const steps = schedule.pipelineSteps; // TypeScript narrows correctly

12. Missing Zod validation on pipeline_task_ids JSON parse (src/implementations/schedule-repository.ts:540)

  • File: schedule-repository.ts
  • Reviewers: Database, Security, TypeScript
  • Impact: Inconsistent with pipeline_steps validation; bare type assertion on untrusted DB data creates silent type corruption risk
  • Fix: Add PipelineTaskIdsSchema and use .parse() like pipeline_steps:
const PipelineTaskIdsSchema = z.array(z.string().min(1));

if (data.pipeline_task_ids) {
  try {
    const parsed = JSON.parse(data.pipeline_task_ids);
    const validated = PipelineTaskIdsSchema.parse(parsed);
    pipelineTaskIds = validated.map((id) => TaskId(id));
  } catch {
    pipelineTaskIds = undefined;
  }
}

13. No service-level test for cancelSchedule with cancelTasks=true (tests/unit/services/schedule-manager.test.ts:358-384)

  • File: schedule-manager.test.ts
  • Reviewer: Tests
  • Impact: Most complex new path in cancelSchedule (execution history lookup, pipelineTaskIds extraction, event emission) is untested at service layer
  • Fix: Add service-level tests for:
    • Pipeline task cancellation when cancelTasks=true
    • Fallback to taskId when pipelineTaskIds absent
    • Proper event emission for each task

RECOMMENDATION

CHANGES_REQUESTED - The blocking issues should be resolved before merge:

Priority 1 (Critical/High - Required):

  1. Update TASK-DEPENDENCIES.md to match actual behavior
  2. Fix duplicated afterScheduleId logic (DRY violation)
  3. Fix duplicated validateScheduleTiming logic (DRY violation)
  4. Wrap pipeline task creation in transaction (data integrity)
  5. Rewrite MCP adapter tests to use real adapter (test coverage)
  6. Create RELEASE_NOTES_v0.6.0.md (release blocker)
  7. Add JSDoc for public API surface

Priority 2 (Medium - Should Fix):
8. Add Zod validation for pipeline_task_ids (type safety)
9. Add service-level test for cancelTasks=true (coverage)
10. Fix TypeScript non-null assertions (type safety)
11. Fix CLI validation duplication (code quality)
12. Document pipeline cleanup architectural exception (maintainability)


POSITIVE OBSERVATIONS

  • Well-structured decomposition of handleScheduleTriggered into handleSingleTaskTrigger, handlePipelineTrigger, and shared helpers
  • Clean domain model extension (pipelineSteps on Schedule, pipelineTaskIds on ScheduleExecution)
  • Targeted, well-placed dependency cascade fix in DependencyHandler
  • Strong Zod validation at MCP boundary (step counts 2-20, path validation, agent enum)
  • SQL injection prevention via parameterized statements throughout
  • Immutable data patterns, Result types, proper error handling
  • Comprehensive test coverage for pipeline trigger logic, dependency cascade, queue fast-path

Files Requiring Changes

  • docs/TASK-DEPENDENCIES.md - Update cascade behavior documentation
  • docs/releases/RELEASE_NOTES_v0.6.0.md - Create new file
  • src/services/schedule-manager.ts - Refactor createSchedule to use validateScheduleTiming
  • src/services/handlers/schedule-handler.ts - Fix afterScheduleId duplication, wrap task creation in transaction, remove non-null assertion
  • src/implementations/schedule-repository.ts - Add Zod validation for pipeline_task_ids
  • src/cli/commands/schedule.ts - Remove duplicated missedRunPolicy ternary
  • src/core/interfaces.ts - Add JSDoc for cancelSchedule and createScheduledPipeline
  • tests/unit/adapters/mcp-adapter.test.ts - Rewrite tests to use real adapter
  • tests/unit/services/schedule-manager.test.ts - Add cancelTasks=true service-level tests

Claude Code - Code Review for Backbeat PR #80

Dean Sharon and others added 7 commits March 11, 2026 17:06
…k in handleSchedulePipeline

The handleSchedulePipeline handler used undefined as the nextRunAt
fallback, while all other schedule handlers (handleScheduleTask,
handleListSchedules, handleGetSchedule) used null. This caused
inconsistent JSON serialization — undefined omits the field entirely,
while null includes it explicitly as "nextRunAt": null.

Co-Authored-By: Claude <noreply@anthropic.com>
…hedule

Replace ~80 lines of inline timing validation in createSchedule with a
call to the existing validateScheduleTiming helper. The helper's JSDoc
already claims it is shared between createSchedule and
createScheduledPipeline, but createSchedule was never refactored to
use it. Now both methods follow the same pattern.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace bare `as string[]` type assertion with PipelineTaskIdsSchema
Zod validation, matching the pattern used by pipeline_steps. This
ensures malformed JSON (e.g., non-string elements, empty strings)
is caught at the database boundary rather than silently propagated.

Co-Authored-By: Claude <noreply@anthropic.com>
…lease notes

TASK-DEPENDENCIES.md contradicted running code: the v0.6.0 cascade
cancellation behavior (failed/cancelled deps auto-cancel dependents)
was not reflected in the Event Flow diagram, Error Handling examples,
or Best Practices section. Updated all sections to match the actual
DependencyHandler implementation.

Created missing RELEASE_NOTES_v0.6.0.md covering scheduled pipelines,
dependency cascade fix, queue handler race condition fix, migration 8,
SchedulePipeline MCP tool, and CLI pipeline flags.

Co-Authored-By: Claude <noreply@anthropic.com>
…type-safety

- Extract resolveAfterScheduleTaskId() returning TaskId | undefined,
  replacing the old resolveAfterScheduleDependency() that returned a
  modified task template. Both single-task and pipeline paths now call
  the same shared helper, eliminating ~18 lines of duplicated logic.
- Pass pipelineSteps as a typed parameter (NonNullable) to
  handlePipelineTrigger, removing the non-null assertion (!).
- Add ARCHITECTURE EXCEPTION comment to pipeline cleanup code explaining
  why direct taskRepo.update() is correct (no events emitted yet).
- Add TODO for async transaction wrapping (better-sqlite3 limitation).

Co-Authored-By: Claude <noreply@anthropic.com>
…rage

Add 3 tests for the cancelTasks branch in ScheduleManagerService.cancelSchedule():
- Pipeline execution with pipelineTaskIds emits TaskCancellationRequested per task
- Single taskId fallback when no pipelineTaskIds present
- cancelTasks=false does not emit TaskCancellationRequested

Co-Authored-By: Claude <noreply@anthropic.com>
Comment on lines +181 to +204
if (cancelTasks) {
const historyResult = await this.scheduleRepository.getExecutionHistory(scheduleId, 1);
if (historyResult.ok && historyResult.value.length > 0) {
const latestExecution = historyResult.value[0];
const taskIds = latestExecution.pipelineTaskIds ?? (latestExecution.taskId ? [latestExecution.taskId] : []);
for (const taskId of taskIds) {
const cancelResult = await this.eventBus.emit('TaskCancellationRequested', {
taskId,
reason: `Schedule ${scheduleId} cancelled`,
});
if (!cancelResult.ok) {
this.logger.warn('Failed to cancel pipeline task', {
taskId,
scheduleId,
error: cancelResult.error.message,
});
}
}
this.logger.info('Cancelled in-flight pipeline tasks', {
scheduleId,
taskCount: taskIds.length,
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancelTasks only covers the single latest execution

getExecutionHistory(scheduleId, 1) limits the lookup to one record. For CRON schedules with short intervals and slow multi-step pipelines, it is possible for a second trigger to fire before the first pipeline finishes. In that situation --cancel-tasks / cancelTasks: true will cancel the newly-created tasks from the latest run but leave the still-running tasks from the previous run untouched.

Consider fetching a small window (e.g., the last 5 executions) and deduplicating task IDs before cancelling:

const historyResult = await this.scheduleRepository.getExecutionHistory(scheduleId, 5);
if (historyResult.ok) {
  const taskIdSet = new Set<TaskId>();
  for (const execution of historyResult.value) {
    if (execution.pipelineTaskIds) {
      execution.pipelineTaskIds.forEach((id) => taskIdSet.add(id));
    } else if (execution.taskId) {
      taskIdSet.add(execution.taskId);
    }
  }
  for (const taskId of taskIdSet) { ... }
}

At a minimum the doc-comment / tool description should mention that only the most recent execution is targeted.

Dean Sharon added 5 commits March 11, 2026 22:52
Chained schedules (afterScheduleId) resolve the predecessor's
execution.taskId to check if it's terminal. Storing firstTaskId
caused the chain to fire when step 1 completed, not the full
pipeline. Now stores lastTaskId so chaining waits for pipeline
completion.
…ascade check

When getDependencies returned an error, code fell through to the
unblock path — potentially unblocking a task whose dependency
actually failed. Now logs a warning and skips the task instead.
…ectory

validatePath() resolves relative/symlink paths to absolute, but the
validation loop discarded the result — schedule stored the original
un-normalized path. Now builds normalizedSteps array with resolved
paths from validatePath().
…iledExecution

Helper hardcoded "Failed to create task: " prefix. Pipeline callsite
already passed "Pipeline failed at step N: ..." — resulting in
double-wrapping in the audit trail. Moved prefix to single-task
callsite, helper now passes errorMessage directly.
Field name implied task IDs but carried a boolean. Renamed to
cancelTasksRequested to accurately describe its semantics.
…ernary

Replace two identical 7-line ternary chains in schedule create (single-task
and pipeline modes) with the existing toMissedRunPolicy() from schedule-manager.
Guards with undefined check to preserve CLI pass-through semantics.
…eline error

Step 0 is the only task that becomes runnable — all later steps block on it.
If its TaskDelegated emit fails, the pipeline is orphaned forever. Now cancels
all saved tasks instead of continuing best-effort.

Also warn when positional prompt words are silently ignored in --pipeline mode,
and fix Biome formatting in schedule-manager test.
Prevents silently discarding pipeline steps when --pipeline is omitted.
Now exits with a helpful error message pointing to correct usage.
Comment on lines +412 to +414
// Update schedule state
const updateResult = await this.updateScheduleAfterTrigger(schedule, triggeredAt);
if (!updateResult.ok) return updateResult;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateScheduleAfterTrigger failure orphans all N pipeline tasks

If updateScheduleAfterTrigger fails (e.g. a transient SQLite error), all N pipeline tasks have already been saved to the DB and the execution has been recorded as 'triggered' via recordTriggeredExecution. But because the return happens before the TaskDelegated emit loop, no task is ever delegated:

  • Step 0 is stuck in pending — never enqueued, never picked up by a worker.
  • Steps 1–N are blocked on step 0 forever — their dependencies will never resolve.
  • The execution record shows 'triggered' with all task IDs, even though nothing will run.

For CRON schedules this is especially bad: because the scheduleRepo.update() call that advances nextRunAt failed, the schedule's nextRunAt is still in the past. On the very next scheduler tick findDue() picks it up again, handlePipelineTrigger runs again, and a new batch of orphaned tasks accumulates. This loop repeats indefinitely until updateScheduleAfterTrigger finally succeeds — potentially creating hundreds of ghost task sets.

Consider adding a cleanup block before returning:

const updateResult = await this.updateScheduleAfterTrigger(schedule, triggeredAt);
if (!updateResult.ok) {
  // Clean up saved tasks so they don't accumulate as orphans
  for (const savedTask of savedTasks) {
    await this.taskRepo.update(savedTask.id, { status: TaskStatus.CANCELLED });
  }
  return updateResult;
}

The same gap exists in handleSingleTaskTrigger (one orphaned task), but it is far less severe than up-to-20 tasks accumulating with every tick.

Comment on lines +549 to +552
} catch {
// Non-fatal: log but don't fail
pipelineTaskIds = undefined;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment says "log" but no logging actually happens

The inline comment reads "Non-fatal: log but don't fail", but SQLiteScheduleRepository has no logger, so silently setting pipelineTaskIds = undefined is the actual behavior. This matters operationally: when cancelTasks: true is used, cancelSchedule falls back to only the single taskId from the execution record (the last task ID), leaving all intermediate pipeline tasks running — and there's no log entry to indicate that the pipelineTaskIds field was silently dropped.

Note the asymmetry with rowToSchedule, which throws on a corrupt pipeline_steps column. Since a corrupt pipelineTaskIds has a real functional impact (incomplete cancellation), it warrants at minimum a console.warn or a dedicated error-logging hook:

} catch (e) {
  // Non-fatal: don't fail execution history reads, but warn so operators can
  // detect DB corruption before it silently breaks --cancel-tasks.
  console.warn(`[schedule-repository] Failed to parse pipeline_task_ids for execution ${data.id}:`, e);
  pipelineTaskIds = undefined;
}

@dean0x dean0x merged commit 19f7383 into main Mar 12, 2026
2 checks passed
@dean0x dean0x deleted the feat/scheduled-pipelines-78 branch March 12, 2026 07:05
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.

feat: scheduled pipelines — run multi-step DAGs on cron/one-time triggers

1 participant