Skip to content

fix(server): archiving a thread stops only its own agent-browser daemon, over its socket - #56

Merged
immakermatty merged 4 commits into
mainfrom
dev6646/daemon-socket-stop
Oct 12, 2026
Merged

immakermatty merged 4 commits into
mainfrom
dev6646/daemon-socket-stop

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 12, 2026 •

Copy link
Copy Markdown

Status: ready for review

Problem

Archiving or deleting a thread stops its agent-browser daemon so the thread's tabs can end (F47 point 5, task 501). The overlay sent SIGTERM to the process the thread's .pid named, after checking that /proc/<pid>/cmdline was agent-browser's. A stale t3-<a>.pid whose number another session's agent-browser daemon has now would stop that daemon and could release its tab. The check and the signal also race. Pablo found the same path in Lazurio/LazurioPlatform#348 (lazurio browser done); this is the overlay's copy.

Fix

  • stopSessionDaemon (in apps/server/src/lazurio/holders.ts) sends agent-browser's own graceful shutdown, {"action":"__agent_browser_internal_shutdown"}, on the session's own socket <socket dir>/t3-<thread>.sock. agent-browser's CLI sends the same in request_graceful_daemon_shutdown before it restarts a daemon. It then waits up to 5 s for the .pid to go. The daemon of an attached browser disconnects without closing a tab and removes its .pid, so the binding stops counting for the hub. No process id is signalled.
  • Outcomes: stopped only once the .pid is gone; none when nothing listens on the socket (no daemon, or a stale socket file); refused when the daemon answers that it did not shut down; unconfirmed when no answer came after the command went out or the .pid is still there after 5 s, so the daemon may still hold the tab. threadTabs.ts logs thread-tabs-daemon-stop-failed with the outcome for the last two, replacing thread-tabs-daemon-not-agent-browser.
  • The fork runbook's "Archivace a smazání" says so.

Not changed

  • Which threads' daemons stop (archive, delete, and threads no longer live at start), the holder file removal, and every name outside t3-.
  • agent-browser close is still never called, because it starts a daemon first when none runs.

Verification

  • vp test run src/lazurio/holders.test.ts src/lazurio/threadTabs.test.ts (apps/server): 53 passed.
    • A live process named agent-browser (a symlink to /bin/sleep) has the number a stale t3-a.pid names, and only t3-b's daemon listens: stopping t3-a answers none, t3-b hears nothing and the process stays alive.
    • A stale socket file answers none; refusal and no answer give failed.
    • t3-a's own daemon gets exactly one shutdown line, and its .pid goes.
    • In the archive scenario, a live agent-browser behind B's stale .pid is untouched.
  • vp lint and vp fmt --check on the changed files: clean. The CI typecheck vp run --filter t3 typecheck: 0 errors. The first head failed it on globalTimers/globalDate; holders.ts now opts out like the other overlay files that run outside the Effect runtime.
  • Not yet: an archive on the canary Environment with the next preview.

This should land before the stable 0.0.45-lazurio.8, which would otherwise ship the PID path.

Plan: DEV-6646 task 2026-10-12-702 (the Platform side is Lazurio/LazurioPlatform#348).

Claude Opus 5.5 in Claude Code.

🤖 Generated with Claude Code

immakermatty and others added 3 commits October 12, 2026 16:23
…on, over its socket

Archiving or deleting a thread stopped its agent-browser daemon with SIGTERM
to the process its .pid named, after checking that the command line was
agent-browser's. A stale .pid whose number another session's daemon has now
would stop that daemon and could release its tab, and the check and the
signal raced (found in LazurioPlatform#348, which had the same path).

stopSessionDaemon now sends agent-browser's own graceful shutdown
(__agent_browser_internal_shutdown, as its CLI does before restarting a
daemon) on the session's own socket, t3-<thread>.sock, and waits up to 5 s
for the .pid to go. Nothing listening means no daemon; a refusal or no
answer is "failed" and is logged. No process id is signalled. The tests run a
live process named agent-browser whose id a stale t3-a.pid names, with only
t3-b's daemon listening: archiving touches neither.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct runtime

The socket shutdown waits for the daemon's answer and for its .pid to go
with setTimeout and Date.now, as the Platform's copy does. The fork's
typecheck flags both (globalTimers, globalDate) unless the file opts out;
holders.ts now does, like the other overlay files that run outside the
Effect runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s gone

As in LazurioPlatform#348 after Pablo's review: an acknowledged shutdown
whose .pid is still there after the wait, or a lost answer after the
command went out, is "unconfirmed" (the daemon may still hold the tab),
and a daemon that answers it did not shut down is "refused". Both are
logged as thread-tabs-daemon-stop-failed with the outcome; only a gone .pid
is "stopped".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai 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.

@immakermatty Exact-head review of be5e8e5 (base b122de4). The stale-PID cross-session regression, archive/delete/startup call paths, t3-only filter, and runbook look aligned; CI is green and review threads are clear. Two error-path regressions violate the stated outcomes:

  1. apps/server/src/lazurio/holders.ts:421-425,457-464: fileExists turns every fs.access rejection into false. In an isolated Node 26 fixture, I wrote t3-a.pid, had ask() return success while changing its parent directory mode to 000, and stopSessionDaemon returned "stopped" while t3-a.pid was still present (fs.access rejected EACCES; restoring the directory proved the file persisted). This violates the completion rule that only a gone .pid counts as stopped. Treat ENOENT as gone; permission/IO errors must be unconfirmed (or throw so the caller logs a failure), not stopped. Add a regression test for inaccessible .pid/parent.

  2. apps/server/src/lazurio/holders.ts:417: askDaemonSocket maps every pre-connect error to "absent". An isolated listening Unix server with its t3-a.sock mode set to 000 returned {kind:"absent"} while server.listening was true. EACCES is not "nothing listens"; surface it as unconfirmed/failure rather than "none", and cover that case. Reserve "absent" for ENOENT/ECONNREFUSED (and other proven absence).

The focused suite was not rerun locally: vp/dependencies are absent in this disposable checkout and host memory pressure is high. I verified the two paths with locked, isolated Node fixtures, inspected the diff, and confirmed the exact-head CI run 38264853671 succeeds. Please fix these outcomes on a new head and repeat exact-head checks before merge/release.

…no daemon or stopped

Pablo's review of #56: every connection error before connect counted as
"nothing listens", so a daemon whose socket cannot be opened (EACCES) was
reported as none; and every failure of fs.access counted as "the .pid is
gone", so an acknowledged shutdown whose .pid cannot be checked was
reported as stopped. Only ENOENT, ECONNREFUSED and ENOTSOCK now prove that
nothing listens, and only ENOENT proves the .pid gone; any other error is
unconfirmed. Tests cover a listening socket of mode 000 and a .pid in a
directory that cannot be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai 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.

@immakermatty APPROVED on exact head 4988c8f (base/current main b122de4). Re-review of my be5e8e5 blockers: fileExists treats only ENOENT as gone and failed PID checks return unconfirmed; the socket maps only ENOENT/ECONNREFUSED/ENOTSOCK before connect to absent, with permission/other errors unconfirmed. I exercised the exact source in a locked isolated Node 26 fixture: a stale A PID naming a live agent-browser process with only B listening returned none, B heard nothing and the process survived; chmod-000 listening socket and inaccessible-but-existing PID both returned unconfirmed; refusal, no answer, gone PID and rejected traversal behaved as specified. I also traced archive/delete/startup sweep and t3-only selection and checked the runbook. git diff --check clean; current-main merge-tree clean; CI run 38268788945 is green on this SHA (format, server/web typecheck, overlay tests and builds). No unresolved threads or other reviewer requests. Native vp tests were not rerun in this disposable checkout because vp/dependencies are absent; focused source fixture and exact-head CI are the execution evidence. This approves the PR head, not a release or any future head.

immakermatty added a commit to Lazurio/LazurioPlatform that referenced this pull request Oct 12, 2026
…aemon or stopped

Pablo's review of Lazurio/t3code#56 found two error paths in the overlay's
copy of stopSessionDaemon, and the Platform's (#348, tagged for rc.58) has
them too: every connection error before connect counted as "nothing
listens", so a daemon whose socket cannot be opened (EACCES) was reported
as none; and every failure of access() counted as "the .pid is gone", so
an acknowledged shutdown whose .pid cannot be checked was reported as
stopped.

Only ENOENT, ECONNREFUSED (Linux) and ENOTSOCK (macOS) now prove that
nothing listens, and only ENOENT proves the .pid gone; any other error is
unconfirmed. The test covers a listening socket of mode 000 and a .pid in a
directory that cannot be read; it fails on main's window.ts. F47's
addendum names the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty
immakermatty merged commit a6915a2 into main Oct 12, 2026
6 checks passed
immakermatty added a commit to Lazurio/LazurioPlatform that referenced this pull request Oct 12, 2026
… MIT boundary (Pablo's second review)

- A newer /.lazurio/shell.js inside an older app could have announced
  majors its app's own parser does not read, since the identity responder
  comes from the shell and the parser from the app. The responder now names
  only the majors the app hands it, so a newer shell inside an older app
  answers without majors and gets v1; the vectors test that combination
  against the Launchpad's and T3 Code's current parsers.
- The MIT exception lists its three parts: the module's directory, the
  artifact's browserContract files and, by Matěj's grant of 2026-10-12
  ("MIT i pro kopii"), the functions T3 Code's overlay copied from holds.ts
  at 49d54c9. The MIT terms ship as the module's own licence file; the
  repository's root LICENSE stays Elastic-2.0 and is never shipped as the
  contract's.
- Phase 1c lists #347, #348, #349 and #351 as merged, and phase 2 names
  Lazurio/t3code#56 as merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
immakermatty added a commit to Lazurio/LazurioPlatform that referenced this pull request Oct 12, 2026
… MIT boundary (Pablo's second review)

- A newer /.lazurio/shell.js inside an older app could have announced
  majors its app's own parser does not read, since the identity responder
  comes from the shell and the parser from the app. The responder now names
  only the majors the app hands it, so a newer shell inside an older app
  answers without majors and gets v1; the vectors test that combination
  against the Launchpad's and T3 Code's current parsers.
- The MIT exception lists its three parts: the module's directory, the
  artifact's browserContract files and, by Matěj's grant of 2026-10-12
  ("MIT i pro kopii"), the functions T3 Code's overlay copied from holds.ts
  at 49d54c9. The MIT terms ship as the module's own licence file; the
  repository's root LICENSE stays Elastic-2.0 and is never shipped as the
  contract's.
- Phase 1c lists #347, #348, #349 and #351 as merged, and phase 2 names
  Lazurio/t3code#56 as merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty
immakermatty deleted the dev6646/daemon-socket-stop branch October 13, 2026 19:21
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