Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,8 @@ bin/fm-teardown.sh <id>
```

The script refuses if the worktree holds uncommitted changes or committed work that has not landed; treat a refusal as a stop-and-investigate, not an obstacle.
Teardown validates that the recorded project and worktree are exact roots with the expected repository registration, quiesces every ordinary task endpoint, and then runs the final non-destructive safety checks before any Treehouse return.
Teardown normally validates that the recorded project and worktree are exact roots with the expected repository registration, quiesces every ordinary task endpoint, and then runs the final non-destructive safety checks before any Treehouse return.
It can also finish bookkeeping for a provably landed worktree whose Treehouse lease was already cleared; `bin/fm-teardown.sh`'s header owns the fail-closed recovery contract.
For a task whose metadata carries `report_required=1`, teardown also publishes the validated completion report before releasing the account lease or removing the worktree.
A safety refusal after quiescence leaves the endpoint stopped while preserving all task state for repair and retry.
`bin/fm-teardown.sh`'s header owns the full landed-work definition (remote-reachable, merged-PR-head containment for the squash-merge-then-delete-branch flow, content already in the default branch, local-only merges) and the `pr=` discovery fallback for merges that skipped `bin/fm-pr-check.sh`.
Expand Down
11 changes: 11 additions & 0 deletions bin/fm-process-tree-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ fm_run_bounded() {
if (!$command) {
close $status_write;
close $finish_read;
# Close inherited fds above stderr so the bounded command starts
# with a clean fd table. The boundary walker in
# fm_checkout_treehouse_return_locked opens one fd per directory
# in the worktree; inherited fds from the parent bash process
# consume headroom and cause EMFILE on large trees.
if (opendir my $devfd, "/dev/fd") {
my @inherited = grep { $_ > 2 }
map { /^(\d+)$/ ? $1 : () } readdir $devfd;
closedir $devfd;
POSIX::close($_) for @inherited;
}
$SIG{HUP} = "DEFAULT";
$SIG{INT} = "DEFAULT";
$SIG{QUIT} = "DEFAULT";
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2817,7 +2817,7 @@ if [ "$KIND" = secondmate ]; then
fi
if [ "$ACCOUNT_EFFECTIVE_MODE" = enforce ]; then
if ! secondmate_home_supports_account_routing "$PROJ_ABS"; then
echo "error: refusing account-routed secondmate launch for $PROJ_ABS: the home lacks Agent Fleet routing support. Fast-forward or otherwise reconcile the home to this Firstmate revision, run bin/fm-config-push.sh, and retry." >&2
echo "error: refusing account-routed secondmate $ID launch for $PROJ_ABS: the home lacks Agent Fleet routing support. Fast-forward or otherwise reconcile the home to this Firstmate revision, run bin/fm-config-push.sh, and retry." >&2
exit 1
fi
elif ! secondmate_home_supports_account_routing "$PROJ_ABS"; then
Expand Down
36 changes: 36 additions & 0 deletions tests/fm-teardown.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4937,6 +4937,41 @@ test_secondmate_registry_updates_are_locked_and_literal() {
pass "secondmate registry updates are serialized and compare ids literally"
}

test_fd_leak_under_low_ulimit() {
local case_dir rc dir_count i
case_dir=$(make_case fd-leak-ulimit)
write_meta "$case_dir" no-mistakes ship
wt_commit "$case_dir" "fix the thing"
git -C "$case_dir/wt" push -q origin fm/task-x1

dir_count=200
i=0
while [ "$i" -lt "$dir_count" ]; do
mkdir -p "$case_dir/wt/deep/dir_$i"
: > "$case_dir/wt/deep/dir_$i/.keep"
i=$((i + 1))
done
git -C "$case_dir/wt" add -A
git -C "$case_dir/wt" -c user.email=t@t -c user.name=t commit -q -m "add dirs"
git -C "$case_dir/wt" push -q origin fm/task-x1

set +e
(
ulimit -n 256 2>/dev/null || exit 99
run_teardown "$case_dir" > "$case_dir/stdout" 2> "$case_dir/stderr"
)
rc=$?
set -e

[ "$rc" -eq 0 ] || fail "fd-leak-ulimit: teardown failed under low ulimit (rc=$rc): $(cat "$case_dir/stderr")"
pass "teardown succeeds under low ulimit -n with many directories (fd leak fix)"
}

if [ "${FM_TEST_FOCUSED:-}" = fd-leak-ulimit ]; then
test_fd_leak_under_low_ulimit
exit 0
fi

if [ "${FM_TEST_FOCUSED:-}" = tasktmp-safety ]; then
test_teardown_removes_safe_tasktmp_and_accepts_absence
test_teardown_refuses_unsafe_tasktmp_metadata
Expand Down Expand Up @@ -5239,3 +5274,4 @@ test_transient_index_lock_clears_after_first_attempt_and_retry_succeeds
test_persistent_index_lock_exhausts_retries_and_refuses_loudly
test_empty_retry_wait_uses_default_without_aborting
test_fractional_legacy_retry_wait_refuses_without_arithmetic_error
test_fd_leak_under_low_ulimit