Before submitting
Area
apps/server
Steps to reproduce
-
On macOS, check the default per-process open-file limit available to a user LaunchAgent:
On my host the soft limit is 256:
-
Install or update the T3 background service:
npx t3@nightly service update
-
Confirm that the generated plist has no per-job open-file override:
plutil -extract SoftResourceLimits.NumberOfFiles raw \
"$HOME/Library/LaunchAgents/com.t3tools.t3code.service.plist"
This fails because SoftResourceLimits.NumberOfFiles is absent.
-
Start T3 through the LaunchAgent and use it with the normal T3 data directory and project set. On this host, startup hit EMFILE in the T3 user-data watcher on two consecutive nightly versions.
This last step depends on the number of descriptors/watchers needed by a particular T3 installation, so it may not fail on every fresh profile. The service-definition check is deterministic; the runtime failure was observed twice, on 0.0.36-nightly.20260827.1206 and 0.0.36-nightly.20260828.1208.
Expected behavior
The macOS background service should be able to start its required file watchers under the resource limits established by its generated LaunchAgent. If the required resources cannot be obtained, the service should fail in a clear and actionable way rather than losing a watcher during startup.
Actual behavior
The generated LaunchAgent inherits the host's soft maxfiles limit of 256. T3 then logged EMFILE: too many open files, watch while creating a FileSystem.watch on its user-data directory.
The server continued startup afterward in the captured runs, but the watcher failure makes the resulting state unclear and can leave settings or user-data changes unwatched.
Running service update recreates the plist without a locally added resource limit, so the exposure returns after an otherwise normal service repair/update.
Impact
Minor bug or occasional failure
Version or commit
Runtime failure observed in 0.0.36-nightly.20260827.1206 and 0.0.36-nightly.20260828.1208. The current plist renderer still emits no SoftResourceLimits on main at d29c56a5c404cb0f58d3b2ac41762fa0d0ac28d4.
Environment
macOS 26.6.2 (Apple Silicon), launchd user agent, Homebrew Node; current host soft maxfiles limit: 256
Logs or stack traces
PlatformError: Unknown: FileSystem.watch (<HOME>/.t3/userdata)
at systemError (file://<HOME>/.t3/runtime/versions/0.0.36-nightly.20260827.1206/node_modules/t3/dist/Path-DtM9tupg.mjs:142:34)
at FSWatcher.<anonymous> (file://<HOME>/.t3/runtime/versions/0.0.36-nightly.20260827.1206/node_modules/t3/dist/NodeServices-BkTDLo1l.mjs:1546:33)
at FSWatcher.emit (node:events:514:20)
at FSWatcher._handle.onchange (node:internal/fs/watchers:275:12)
at server.startup.settings.start (file://<HOME>/.t3/runtime/versions/0.0.36-nightly.20260827.1206/node_modules/t3/dist/bin.mjs:67632:99) {
[cause]: Error: EMFILE: too many open files, watch
at FSWatcher._handle.onchange (node:internal/fs/watchers:269:21)
}
The same failure appeared again with 0.0.36-nightly.20260828.1208 at the same FileSystem.watch boundary.
Investigation notes
I inspected the generated plist, the loaded launchd environment, the historical boot-service log, and the current renderer.
launchctl limit maxfiles reports a soft limit of 256 on this host.
- The generated plist does not contain
SoftResourceLimits.NumberOfFiles.
- The error originates while T3 starts a watcher for
<HOME>/.t3/userdata, not from an unrelated project command.
- The current renderer deliberately configures several launchd-specific lifecycle settings, but it does not configure an open-file limit:
|
export function renderBootServicePlist( |
|
plan: BootServicePlan, |
|
options: { readonly homeDir: string; readonly environmentPath: string }, |
|
): string { |
|
// KeepAlive + ThrottleInterval mirror Restart=always + RestartSec=5. launchd |
|
// has no StartLimitBurst analog; a hard crash loop respawns every 5s forever. |
|
// ExitTimeOut 90 matches systemd's default TimeoutStopSec. A plain stop |
|
// completes within the launcher's 5s child grace, but a stop that queues |
|
// behind an in-flight update transition can take much longer; launchd's |
|
// system-defined default (5s on current macOS) would SIGKILL the launcher |
|
// (and, with it, the process group) mid-handoff. |
|
// ProcessType Interactive opts out of background-job resource throttling. |
|
// AbandonProcessGroup stays at its default (false): launchd reaps leftover |
|
// process-group members only when the launcher itself exits — the analog of |
|
// KillMode=mixed's final cgroup kill — and not when the launcher restarts its |
|
// child, so agent children survive server updates. |
|
return [ |
|
`<?xml version="1.0" encoding="UTF-8"?>`, |
|
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`, |
|
`<plist version="1.0">`, |
|
`<dict>`, |
|
` <key>Label</key>`, |
|
` <string>${BOOT_SERVICE_LAUNCHD_LABEL}</string>`, |
|
` <key>ProgramArguments</key>`, |
|
` <array>`, |
|
` <string>${escapeXmlText(plan.nodePath)}</string>`, |
|
` <string>${escapeXmlText(plan.launcherPath)}</string>`, |
|
` </array>`, |
|
` <key>EnvironmentVariables</key>`, |
|
` <dict>`, |
|
` <key>PATH</key>`, |
|
` <string>${escapeXmlText(options.environmentPath)}</string>`, |
|
` <key>T3CODE_HOME</key>`, |
|
` <string>${escapeXmlText(plan.baseDir)}</string>`, |
|
` <key>${BOOT_SERVICE_UNIT_ENV}</key>`, |
|
` <string>${BOOT_SERVICE_PLIST_FILE}</string>`, |
|
` </dict>`, |
|
` <key>WorkingDirectory</key>`, |
|
` <string>${escapeXmlText(options.homeDir)}</string>`, |
|
` <key>RunAtLoad</key>`, |
|
` <true/>`, |
|
` <key>KeepAlive</key>`, |
|
` <true/>`, |
|
` <key>ThrottleInterval</key>`, |
|
` <integer>5</integer>`, |
|
` <key>ExitTimeOut</key>`, |
|
` <integer>90</integer>`, |
|
` <key>ProcessType</key>`, |
|
` <string>Interactive</string>`, |
|
` <key>StandardOutPath</key>`, |
|
` <string>${escapeXmlText(plan.logPath)}</string>`, |
|
` <key>StandardErrorPath</key>`, |
|
` <string>${escapeXmlText(plan.logPath)}</string>`, |
|
`</dict>`, |
|
`</plist>`, |
|
``, |
I cannot tell from outside the project whether the preferred approach is to change the LaunchAgent resources, reduce descriptor pressure, change watcher behavior, or surface a clearer startup failure. A locally higher per-job limit stopped the errors in my observations, but that correlation is not intended as a prescribed upstream value or implementation.
Workaround
I currently maintain a local per-job open-file limit in the LaunchAgent and verify it after service updates. The exact value is local policy, not a proposed T3 default.
Before submitting
Area
apps/server
Steps to reproduce
On macOS, check the default per-process open-file limit available to a user LaunchAgent:
On my host the soft limit is 256:
Install or update the T3 background service:
Confirm that the generated plist has no per-job open-file override:
plutil -extract SoftResourceLimits.NumberOfFiles raw \ "$HOME/Library/LaunchAgents/com.t3tools.t3code.service.plist"This fails because
SoftResourceLimits.NumberOfFilesis absent.Start T3 through the LaunchAgent and use it with the normal T3 data directory and project set. On this host, startup hit
EMFILEin the T3 user-data watcher on two consecutive nightly versions.This last step depends on the number of descriptors/watchers needed by a particular T3 installation, so it may not fail on every fresh profile. The service-definition check is deterministic; the runtime failure was observed twice, on
0.0.36-nightly.20260827.1206and0.0.36-nightly.20260828.1208.Expected behavior
The macOS background service should be able to start its required file watchers under the resource limits established by its generated LaunchAgent. If the required resources cannot be obtained, the service should fail in a clear and actionable way rather than losing a watcher during startup.
Actual behavior
The generated LaunchAgent inherits the host's soft
maxfileslimit of 256. T3 then loggedEMFILE: too many open files, watchwhile creating aFileSystem.watchon its user-data directory.The server continued startup afterward in the captured runs, but the watcher failure makes the resulting state unclear and can leave settings or user-data changes unwatched.
Running
service updaterecreates the plist without a locally added resource limit, so the exposure returns after an otherwise normal service repair/update.Impact
Minor bug or occasional failure
Version or commit
Runtime failure observed in
0.0.36-nightly.20260827.1206and0.0.36-nightly.20260828.1208. The current plist renderer still emits noSoftResourceLimitsonmainatd29c56a5c404cb0f58d3b2ac41762fa0d0ac28d4.Environment
macOS 26.6.2 (Apple Silicon), launchd user agent, Homebrew Node; current host soft
maxfileslimit: 256Logs or stack traces
The same failure appeared again with
0.0.36-nightly.20260828.1208at the sameFileSystem.watchboundary.Investigation notes
I inspected the generated plist, the loaded launchd environment, the historical boot-service log, and the current renderer.
launchctl limit maxfilesreports a soft limit of 256 on this host.SoftResourceLimits.NumberOfFiles.<HOME>/.t3/userdata, not from an unrelated project command.t3code/apps/server/src/cloud/bootService.ts
Lines 102 to 157 in d29c56a
I cannot tell from outside the project whether the preferred approach is to change the LaunchAgent resources, reduce descriptor pressure, change watcher behavior, or surface a clearer startup failure. A locally higher per-job limit stopped the errors in my observations, but that correlation is not intended as a prescribed upstream value or implementation.
Workaround
I currently maintain a local per-job open-file limit in the LaunchAgent and verify it after service updates. The exact value is local policy, not a proposed T3 default.