Tuning tman for TypeScript (tsc)

JavaScript / TypeScript

Minutes of total silence on a large project, and --watch must never get a timeout.

capstart atwhat it answers
stall15mhang backstop — no output and no CPU/IO activity for this long
max-time15mwall-clock budget — the only cap that bounds a run that is working but will never finish
max-mem8192RSS ceiling across the process tree
max-parallel2slots per name-and-directory bucket

These are starting points derived from how TypeScript (tsc) behaves, not measurements of your machine. Every one of them should be replaced by a number you took from your own runs — the last section on this page is how.

.tman.kdl
alias "typecheck" {
    command "npx"
    args "tsc" "--noEmit"
    max-time "15m"
    max-mem 8192
}

tsc --noEmit produces no output at all until it produces all of it. On a large project that is minutes of complete silence followed by either nothing or a wall of errors — the exact profile that makes a stall value sized like a runtime budget so destructive.

On Linux this is safe at any reasonable stall, because the type checker burns CPU continuously and tman counts that as activity. On macOS and Windows only the root process is sampled — which is the whole of tsc, so its CPU is still visible, but any work it delegates is not. Keep stall wide there.

`--watch` must never carry a wall-clock cap

A watching typechecker is designed never to exit. Giving it max-time guarantees a kill and an exit 124 that carries no information. What you actually want from supervision here is the dedup lock: an agent that starts a watcher, forgets, and starts another leaves you with two processes racing on the same output.

.tman.kdl
alias "typecheck" {
    command "npx"
    args "tsc" "--noEmit"
    max-time "15m"
}

alias "typecheck-watch" {
    command "npx"
    args "tsc" "--noEmit" "--watch"
    max-parallel 1        // one watcher, and --name refuses the second
}

Heap, and project references

The type checker holds the whole program graph in memory, and on a large monorepo that reaches several gigabytes before Node's own heap limit stops it with an unhelpful abort. A max-mem of 8 GB sits above ordinary work and below the point where the machine suffers. If you are hitting it regularly, project references and tsc --build reduce the working set rather than just accommodating it.

Turn the starting point into a real number

Everything above is derived from how this tool behaves, not from your machine. The numbers that matter come from your own runs, and getting them takes one command:

shell
tman status --json | jq '.runs[] | {name, exit, duration_ms}'

Set max-time from the slowest successful run you are willing to wait for, with real headroom — a cap that fires on healthy work teaches everyone to raise the cap, and after a few rounds of that it bounds nothing. Set stall from the longest stretch the command legitimately produces no output and no CPU, then double it.

Exit 124, 125, and 126 are findings, not flakes. 124 is a wall-clock timeout, 125 a stall, 126 a resource cull. Widening the cap that fired is the right response only after you know why it fired — the first two times a suite trips max-mem, the interesting question is what is holding the memory.

Frequently asked

Can I put max-time on tsc --watch?

No — a watching typechecker is designed never to exit, so max-time guarantees a kill and an exit 124 that means nothing. Give the watch alias max-parallel 1 and a --name instead; the dedup lock is the supervision you actually want, because it stops a second watcher racing the first.

How long can tsc be silent before tman kills it?

At the 30m built-in stall, half an hour of silence *and* idleness. On Linux the check never looks idle because it burns CPU continuously and tman counts that as activity, so the practical answer is that a working tsc is not at risk. On macOS and Windows keep the value wide.

Sources