Skip to content

feat: the daemon is a session claimant, and owning is not writing - #76

Merged
eranra merged 1 commit into
mainfrom
feat/daemon-claimant
Sep 4, 2026
Merged

feat: the daemon is a session claimant, and owning is not writing#76
eranra merged 1 commit into
mainfrom
feat/daemon-claimant

Conversation

@eranra

@eranra eranra commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The gap

Session ownership had three tiers, and the first two both resolve to a VS Code window — one that holds the session, or one whose workspace contains it. On a machine with no window at all, both find nothing and every session fell to tier three: nobody, read-only.

So a terminal-only fleet could be neither listed as owned nor acted on. That is the assumption the whole remote interface rests on, and the one a terminal breaks.

The daemon claims what no window does

session-sitter daemon is now a claimant, below both window tiers. Below, and not because a window is more trustworthy — a window can do strictly more. Claiming first would take a session that could be answered from a phone and hand it to an owner that can only watch.

It claims only while its heartbeat reads running. A wedged daemon would take sessions off the read-only tier and then fail to serve them, and the list would say somebody had.

Owning a session is not the same as being able to write to it

This is the part worth reviewing. canInject is a separate question from basis, because injecting text goes through the agent's own extension host over the V8 inspector — which exists only inside VS Code. The daemon can be responsible for a session, mirror it, answer the permission prompts it raises through hook escalation, and still be unable to type into it.

Conflating the two would have the remote interface offer a button that silently does nothing — against a feature whose stated rule is that it never writes to a session it cannot positively reach, and says why where it cannot. So applyCommand refuses sendText, focus and newSession before calling any sender.

Checking first rather than letting a sender fail is deliberate. The senders' own failures are no-channel and ambiguous, which describe a window that could not find the right conversation — a different problem with a different fix. Reporting one as the other sends someone hunting for a session that was never reachable from here at all.

--owners makes the model observable, from a terminal

   STATUS    SESSION                 AGENT   WORKSPACE       OWNER         UPDATED
!  approval  bump the pinned deps    Claude  infra           daemon 9001        2m
◉  finished  add the retry test      Claude  session-sitter  window 33550       1d
·  dormant   an old experiment       Codex   scratch         read-only          6d

Resolved by exactly the code the panel uses, from files on disk — the window registry and the heartbeat — so a terminal reaches the same answer without being an IDE. Sharing the resolver rather than reimplementing it is the point: two surfaces disagreeing about who holds a session is worse than either being wrong, because then neither can be trusted.

Verified live against this machine's real VS Code window:

$ session-sitter status --owners --json --all | jq '.sessions[0].owner'
{ "kind": "window", "pid": 33550, "basis": "workspace", "canWrite": true }

--json reports owner.canWrite rather than leaving each caller to infer it from basis, and the key is absent without --owners rather than null — absent means nobody looked, null means nothing claims it. There's a test for that distinction.

The Telegram topic header and the /who listing name a daemon owner as a daemon. Calling it a "window" would tell a reader they can type there, which is the one thing they cannot.

Not in this change, and worth being plain about

The daemon does not yet run the Telegram mirror. That needs a host-free session partition — RemoteControlService takes a SessionManager and asks the panel which sessions are active — and porting that is a substantially larger piece than the ownership model. What landed is the model, the capability distinction, the refusals, and one surface that exercises all three. I'd rather say so than imply the mirror works headlessly.

Tests

+51 cases. The ones that matter:

  • the daemon never outranks either window tier, and does not claim a peer machine's session;
  • a wedged daemon (stale, dead, oneshot) claims nothing;
  • every write command refuses without calling the sender, so nothing can half-succeed;
  • a blocker does not mask an ordinary mistake — an empty message still reports "Nothing to send.";
  • owner absent vs null in --json.

Verification

make check (2661 tests, 104 files), make guards, codespell, claude plugin validate --strict, and the settings parity guard from #75 all green.

🤖 Generated with Claude Code

Session ownership had three tiers, and the first two both resolve to a
VS Code window: one that holds the session, or one whose workspace
contains it. On a machine with no window at all both find nothing, so
every session fell to tier three — nobody, read-only. A terminal-only
fleet could be neither listed as owned nor acted on, which is the
assumption the whole remote interface rests on and the one a terminal
breaks.

`session-sitter daemon` is now a claimant, below both window tiers.
Below, and not because a window is more trustworthy: a window can do
strictly more. Claiming first would take a session that could be
answered from a phone and hand it to an owner that can only watch. A
daemon claims only while its heartbeat reads `running` — a wedged one
would take sessions off the read-only tier and then fail to serve them,
and the list would say somebody had.

Owning a session is not the same as being able to write to it, and that
is now explicit rather than assumed. `canInject` is a separate question
from `basis`, because injection goes through the agent's own extension
host over the V8 inspector, which exists only inside VS Code. The daemon
can be responsible for a session, mirror it, answer the permission
prompts it raises through hook escalation, and still be unable to type
into it.

Conflating those would have the remote interface offer a button that
silently does nothing, against a feature whose stated rule is that it
never writes to a session it cannot positively reach and says why where
it cannot. So `applyCommand` refuses sendText, focus and newSession
BEFORE calling any sender, with a sentence naming the fix. Checking
first rather than letting a sender fail matters: the senders' own
failures are `no-channel` and `ambiguous`, which describe a window that
could not find the right conversation — a different problem with a
different fix, and reporting one as the other sends someone hunting for
a session that was never reachable from here at all.

`session-sitter status --owners` makes the model observable from a
terminal, resolved by exactly the code the panel uses from files on disk
— the window registry and the heartbeat. Sharing the resolver rather
than reimplementing it is the point: two surfaces disagreeing about who
holds a session is worse than either being wrong, because then neither
can be trusted. `--json` reports `owner.canWrite` rather than leaving
each caller to infer it from `basis`, and the key is absent without
`--owners` rather than null: absent means nobody looked, null means
nothing claims it.

The Telegram topic header and the `/who` listing name a daemon owner as
a daemon. Calling it a window would tell a reader they can type there,
which is the one thing they cannot do.

Not in this change, and worth being plain about: the daemon does not yet
run the Telegram mirror. That needs a host-free session partition —
RemoteControlService takes a SessionManager and asks the panel which
sessions are active — and porting it is a larger piece than the
ownership model. What landed is the model, the capability distinction,
the refusals, and one surface that uses all three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Eran Raichstein <eranra@il.ibm.com>
@eranra
eranra merged commit 7df8299 into main Sep 4, 2026
7 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.

1 participant