Repository navigation
Conversation
Insomnia --session start <seconds> | extend <seconds> | end | status lets a script or an agent start, extend and end the session the menu bar counts down, with the same battery and heat rules. The command line changes nothing itself: it links one request into session-command in the support directory, the app's watcher claims it, runs it like the matching menu action and answers in session-command.reply.<id>. A request nothing claims within 2 s is taken back, and one older than 5 s is dropped, so a request left behind never starts a session later.
A start queued behind another, an end whose restore failed, and an end that arrived while a start was in flight could all answer done. The manager's tryStart and tryExtend now say whether this call took effect, an end maps every EndOutcome other than restored to a reason, and an end or extend with no session yet waits for queued work first. The watcher starts after the launch reconcile, a request a killed command line left no longer blocks the next one, a command line waits on and takes back only its own file (by inode), old answers are removed, and an app still starting is told apart from an older build.
…nd every time Greptile on krishhgg#99: an end that found no session answered done without ending, which left a dirty journal or an unadopted session.json behind, and the take-back of a shared request path could still drop another caller's request when three command lines raced. An end now always runs end(reason: .user), which also abandons a start in flight. Each command line writes its own session-command.<id>.request, so publishing, the app's claim and a take-back are each one rename of that caller's own file, and the app serves the requests it finds oldest first.
End-to-end test in a VM on macOS 27.0.1Every command did what the PR says, from a shell in the VM. SetupA Tart VM on the maintainer's Mac mini, macOS 27.0.1 (26A434) with Xcode 27.0. Each version was built from source and installed with This PR's head (34f3f43). The script ( Results
After The menu bar after Not covered
The full log is 🤖 Generated with Claude Code |
|
Thanks for this. Two things before it can merge. 1. Merge mainThe branch now conflicts with 2. A start can be refused before an earlier end runsStart with a session running, then send Fix: remove the early check and let Push these as new commits on this branch. CI, Greptile and the Codex review then run again on the new head. My VM test above covered 🤖 Generated with Claude Code |
…st runs first `end` then `start` each run in their own task. While the end waited on the lifecycle queue or the recovery lock, the early `session != nil` check in `run(.start)` still saw the old session and refused the start as already running; the end then ran and left no session. The check is gone, so the start queues behind the end and `performStart` decides under the lock. The check after `tryStart` still gives the refusal message. The new test holds an extend at the backstop arm, queues `end` and then `start`, and checks that the start makes a new session.
`tryStart` refuses a start while an end is pending, but only when the request arrives. An end already in the queue can leave itself to its retry (`scheduleEndRetry` in `performEnd`), and a start queued behind it went on to start a session that the retry stopped 30 s later. `performStart` now checks `pendingEnd` as well, with the same refusal. Removing the early check in `run(.start)` made this reachable from `end` then `start`; a start sent with no session running could already reach it. The new test fails the end's arm, lets the start's succeed, and checks the start is refused and nothing runs.
|
Both done, plus one fix that removing the early check made reachable.
|
|
Thanks for this, @0xRafie. To avoid merge conflicts with the other open pull requests, your branch lands through #146, which merges your head commit e738536 unchanged. When #146 merges, GitHub marks this pull request merged and the commits stay yours. Please don't push to this branch now. A new commit wouldn't be in #146, so this pull request would stay open instead of showing as merged. If something needs changing, comment here or on #146 and it'll go in as a follow-up there. #146 adds one maintainer commit after your merge: 🤖 Generated with Claude Code |
Lands #99 at e738536, by 0xRafie. Conflict with #137 and the maintainer's time change fix, resolved here: each added an app-lifetime object to InsomniaApp, the display sleep and time change observers and #99's session command watcher. All three are kept. The observers start first; the watcher starts after the launch reconcile, as in #99.


Summary
Adds
Insomnia --session start <seconds> | extend <seconds> | end | status, a one-shot mode that drives the running menu bar app's session from a script or an agent. The session it starts is the one the menu bar counts down. It ends from the menu like any other and keeps the battery and heat rules, and the command line never runs pmset itself.The use case: an agent that keeps working with the lid closed can start or extend a session and end it when its process exits, while the person sees the same countdown in the menu bar. Related to #57; this is not Auto, which #79 implements.
How it works
session-command.<id>.request, holdingstart <seconds>,extend <seconds>orend. It writes the file under a private name and renames it into place. No two command lines share a path, so publishing, the app's claim and a take-back are each onerename(2)of that caller's own file.SessionCommandWatcherwatches the support directory, asLidSimulationdoes, from the end of the launch reconcile. It claims each request byrename(2)and drops one that is older than 5 s or malformed. It runs the rest oldest first throughSessionManager.run, which calls the same start, extend and end the menu uses, and answers insession-command.<id>.replyas JSON.tryStartandtryExtendreturn whether this call took effect (startandextendare unchanged wrappers), so a start queued behind another one answers "already running" instead of a false success. An extend that finds no session yet waits for queued work first.end(reason: .user), whether or not the manager holds a session. It abandons a start in flight, finishes an earlier end or a dirty journal, and ends a session.json that a delayed launch recovery has not adopted. EveryEndOutcomeother than.restoredbecomes a reason and exit 1.statusonly reads session.json.Info.plistdeclaresInsomniaSessionCommandVersion1. A build without this mode opens the menu bar app instead, so a script that may meet one checks the key first, the same idea asInsomniaResumeFrozenVersion.Trust boundary
The command file is the same boundary as config.json and the simulate-lid trigger: only this user can write the support directory, and anyone who can write it already controls the app. Any process of this user can also run the four sudoers commands directly; going through the app adds the journal, the backstop and the floors.
Testing
SessionCommandTests:SessionCommandTests(24 tests) through a local XCTest stand-in, and checked each guard by removing it to confirm a test fails. CI runs the whole suite.swift build -c release -Xswiftc -warnings-as-errorspasses. It was built with the macOS 26.5 SDK, because the Command Line Tools' macOS 27 SDK lacks the SwiftUI macro plugin.scripts/install.sh:--session start 120shows the countdown in the menu bar;extend, a refused second start,status,endand hold-to-end each behave as described;docs/releasing.mdhas a new real-Mac check, and README and docs/spec.md describe the mode.

Confidence Score: 5/5
The PR appears safe to merge; no new actionable issues were found.Summary
Adds
Insomnia --sessionso scripts can control the running app’s session.Both earlier findings are addressed:
endalways runs full cleanup, and callers no longer share a request path.Diagram
Reviews (3) · Last reviewed commit: "Session: refuse a start queued behind an..." · Reviewed by Greptile