feat: the daemon is a session claimant, and owning is not writing - #76
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 daemonis 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.
canInjectis a separate question frombasis, 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
applyCommandrefusessendText,focusandnewSessionbefore calling any sender.Checking first rather than letting a sender fail is deliberate. The senders' own failures are
no-channelandambiguous, 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.--ownersmakes the model observable, from a terminalResolved 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:
--jsonreportsowner.canWriterather than leaving each caller to infer it frombasis, and the key is absent without--ownersrather than null — absent means nobody looked, null means nothing claims it. There's a test for that distinction.The Telegram topic header and the
/wholisting 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 —
RemoteControlServicetakes aSessionManagerand 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:
stale,dead,oneshot) claims nothing;ownerabsent 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