Skip to content

feat(projects): make .cascade/setup.sh timeout configurable per project (MNG-1701)#1467

Merged
aaight merged 1 commit into
devfrom
feature/mng-1701-configurable-setup-timeout
Jun 26, 2026
Merged

feat(projects): make .cascade/setup.sh timeout configurable per project (MNG-1701)#1467
aaight merged 1 commit into
devfrom
feature/mng-1701-configurable-setup-timeout

Conversation

@aaight

@aaight aaight commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes the wall-clock timeout of .cascade/setup.sh configurable per project. Adds a new nullable setup_timeout_ms column on projects, plumbed through the proven maxInFlightItems / snapshotTtlMs / workerImage precedent: migration → Drizzle schema → ProjectConfigSchemaconfigMapperprojectsRepository → tRPC projects.create/update → CLI → dashboard form.

Issue: https://linear.app/issue/MNG-1701

Semantics

  • null/unset or 0no per-project wall timeout (rely on the global worker/watchdog container timeout, the current safety net).
  • > 0 → that millisecond value is passed as wallTimeoutMs to the runCommand call for setup.sh.
  • The idle timeout stays disabled (idleTimeoutMs: 0) regardless — the new field only controls the wall timeout.

The behavior lives in one place: setupRepository (src/agents/shared/repository.ts), where input.project is the full ProjectConfig, so project.setupTimeoutMs is already available — no new params threaded through.

⚠️ Default-behavior note for reviewers. This repo's setupRepository already passed { idleTimeoutMs: 0, wallTimeoutMs: 0 } (PR #1463 disabled the wall timeout because npm ci on large React Native repos can take 10–15+ min). This PR preserves that unset/0 default (still wallTimeoutMs: 0 → disabled) and makes it configurable: a positive setupTimeoutMs re-introduces a per-project wall bound. runCommand treats wallTimeoutMs <= 0 as disabled (see createSubprocessWatcher in src/utils/repo.ts).

⚠️ 0 is a first-class value. Unlike maxInFlightItems (.positive()), this field uses .nonnegative() in the Zod config schema and both tRPC inputs, and the CLI uses a !== undefined guard (oclif parses 0 fine, but a truthy check would drop it) so an explicit 0 ("disable") is transmitted, not silently dropped. The web form relies on "0" being a truthy string so it transmits correctly.

Changes

Storage & config plumbing

  • src/db/migrations/0058_add_setup_timeout_ms.sql (new) + _journal.json entry (idx 58, when 1793000000000, tag 0058_add_setup_timeout_ms).
  • src/db/schema/projects.tssetupTimeoutMs: integer('setup_timeout_ms') (nullable, no default).
  • src/config/schema.tssetupTimeoutMs: z.number().int().nonnegative().optional() on ProjectConfigSchema (no PROJECT_DEFAULTS entry — absent/0 means disabled).
  • src/db/repositories/configMapper.tssetupTimeoutMs added to ProjectRow (DB select shape), ProjectConfigRaw, and buildBaseProjectFields (row.setupTimeoutMs ?? undefined).
  • src/db/repositories/projectsRepository.tssetupTimeoutMs?: number | null on both createProject data and updateProject updates param types; setupTimeoutMs: rest.setupTimeoutMs in the create .values({...}) (update already spreads ...rest).
  • src/api/routers/projects.tssetupTimeoutMs: z.number().int().nonnegative().nullish() on both create and update inputs (both already spread input into the repository call).

The actual behavior

  • src/agents/shared/repository.tssetup.sh runCommand now passes { idleTimeoutMs: 0, wallTimeoutMs: project.setupTimeoutMs ?? 0 }; the explanatory comment is rewritten to describe the per-project configurable wall timeout.

CLI + dashboard surfaces

  • src/cli/dashboard/projects/update.ts & create.ts--setup-timeout-ms flag mirroring --max-in-flight-items, passed through with the !== undefined guard.
  • src/cli/dashboard/projects/show.tsSetup Timeout (ms) row in the detail output.
  • web/src/components/projects/project-general-form.tsxsetupTimeoutMs on the local Project interface; state wired into isDirty, handleReset, and handleSubmit (sends setupTimeoutMs ? Number.parseInt(...) : null); a number Input (min="0") labelled "Setup script timeout (ms)" with helper text in the Budget & Limits card.

Tests

  • tests/unit/agents/shared/repository.test.ts — new tests asserting setupTimeoutMs: 1800000wallTimeoutMs: 1800000 and setupTimeoutMs: 0wallTimeoutMs: 0 (the existing no-override test already expects wallTimeoutMs: 0).
  • tests/unit/db/repositories/configMapper.test.tssetupTimeoutMs: null added to baseProjectRow; cases for null → undefined and positive passthrough.
  • tests/unit/cli/dashboard/projects/projects.test.ts--setup-timeout-ms 1800000 on update + create, the 0 passthrough case, and absent-flag cases.
  • tests/integration/db/projectsRepository.test.ts — DB round-trip (create + update + clear), including the explicit-0-distinct-from-null case.

Docs

  • docs/architecture/08-config-credentials.mdsetupTimeoutMs? added to the ProjectConfig interface block.
  • CHANGELOG.md — Unreleased › Added entry.

Testing

  • npm run typecheck (src) + npx tsc -b (web) — clean.
  • Biome lint on changed/new files — clean.
  • Unit: targeted suites + the config/db/cli/agents directories (2400+ tests) pass; new repository.test.ts, configMapper.test.ts, projects.test.ts, projects-router tests pass.
  • Integration: tests/integration/db/projectsRepository.test.ts (50 tests incl. 4 new setup-timeout column tests) pass — the 0058 migration applies cleanly via the journal-driven migrator.
  • Docs drift guard tests/unit/architecture-docs.test.ts passes.

After merge, run npm run db:migrate to apply 0058 to live databases (additive nullable column — current behavior preserved when unset).

🤖 Generated with Claude Code

🕵️ claude-code · claude-opus-4-8 · run details

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@nhopeatall nhopeatall left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

APPROVE — clean, complete implementation of MNG-1701 that faithfully follows the proven maxInFlightItems / snapshotTtlMs / workerImage precedent across every layer, with correct semantics and thorough tests. CI 7/7 green.

I traced the full path rather than pattern-matching the plumbing:

Behavior is correct and backward-compatible

  • src/agents/shared/repository.ts: const setupWallTimeoutMs = project.setupTimeoutMs ?? 0{ idleTimeoutMs: 0, wallTimeoutMs: setupWallTimeoutMs }. Idle stays disabled unconditionally.
  • Confirmed in src/utils/repo.ts that createSubprocessWatcher only arms the wall timer when wallTimeoutMs > 0 (line 169), so 0/unset = disabled. The explicit value also bypasses the ?? DEFAULT_WALL_TIMEOUT_MS fallback in resolveOptions, so passing 0 genuinely disables rather than reverting to the 10-min default.
  • Unset → ?? 0wallTimeoutMs: 0, identical to current post-#1463 behavior — additive nullable column, no behavior change for existing projects.

0 as a first-class value (the one real correctness risk) is handled everywhere

  • Zod config schema + both tRPC inputs use .nonnegative() (not .positive()).
  • CLI uses the !== undefined guard (oclif parses 0 fine; a truthy check would drop it) — verified by the explicit --setup-timeout-ms 0 passthrough tests.
  • Web form relies on "0" being truthy (numericFieldDefault(0)"0"); empty → null. Correct.

Read path verified end-to-end (not just compile-time)

  • getProjectFull / listProjectsFull do db.select().from(projects) (full row), and serializeProject spreads ...rest, so setupTimeoutMs reaches the settings form at runtime — the saved value will actually repopulate the field.
  • loadConfigFromDb / loadProjectConfigByIdmapProjectRow also include it, hydrating the worker's ProjectConfig.

Migration & completeness

  • 0058 is an additive nullable column; journal entry is correct (idx 58 after 57, when 1793000000000 after 1792000000000, tag matches filename).
  • No surface was missed: the other files referencing maxInFlightItems/snapshotTtlMs (pipeline-capacity-gate, snapshot-manager, worker-spawn-settings, backlog-check) are feature-specific consumers; setupTimeoutMs's sole consumer is setupRepository, correctly wired.
  • Authorization correctly mirrors maxInFlightItems (project-admin, not superadmin-gated like workerImage) — appropriate for a benign operational setting.

Tests cover the meaningful cases: positive/0 wall-timeout in repository.test.ts, null→undefined + passthrough in configMapper.test.ts, CLI 0/passthrough/absent on both create+update, and a DB round-trip including the explicit-0-distinct-from-null case.

Docs (08-config-credentials.md, CHANGELOG.md) are accurate. No blocking or should-fix issues found.

🕵️ claude-code · claude-opus-4-8 · run details

@aaight aaight merged commit 78e648d into dev Jun 26, 2026
9 checks passed
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.

2 participants