Skip to content

Add opt-in unattended resilience to the Pane Chat orchestrator skill - #609

Merged
parsakhaz merged 3 commits into
mainfrom
pane-chat-unattended-resilience
Sep 14, 2026
Merged

parsakhaz merged 3 commits into
mainfrom
pane-chat-unattended-resilience

Conversation

@parsakhaz

@parsakhaz parsakhaz commented Sep 14, 2026

Copy link
Copy Markdown
Member

Description

A long unattended /do run died twice last night: the MacBook slept on battery with the lid closed, the Anthropic API became unreachable (ENOTFOUND), the in-flight review subagent stalled, and the turn ended. Pane Chat recovered it without any human input: when the watcher's queued events reached it during a later wake, it read the pane, saw the sleep and ENOTFOUND errors, and sent the pane a resume message on its own judgment. The skill text did not yet describe that move. This PR makes it a standard, opt-in behaviour of the generated orchestrator skill.

Not exercised end to end. No live Pane Chat session has been put through sleep and recovery with this text. The tests prove the text is emitted, not that an agent following it recovers. The startup question will appear on every Pane Chat launch for every install after upgrade.

The change is text only, in main/src/services/skillCacheManager.ts. Two shared constants (the startup prompt and the resilience section) are embedded in both emitted variants so those two blocks cannot drift. The variants are otherwise different documents:

  • the guide (<PANE_DIR>/skills/pane-chat/runpane-orchestrator.md), which has no Liveness Contract of its own and points at the skill's
  • the skill (<PANE_DIR>/skills/pane-chat/pane-orchestrator/SKILL.md), which owns the Liveness Contract and is mirrored byte-for-byte to <PANE_DIR>/.codex/skills and <PANE_DIR>/.claude/skills and converted to the .cursor/rules file

Startup question. In Initialize, after doctor and runpane watch --self-test, the orchestrator asks once per session (the text notes it lives in both files and one ask covers both): "Enable unattended resilience for this session? (keeps the Mac awake with caffeinate, auto-resumes panes whose turn died from a sleep/network API error, re-arms the watcher). Default: yes." The default is yes and the orchestrator does not wait for an answer. Any other prompt counts as yes, said in one line. An explicit "no" at any point disables it for the rest of the session and kills caffeinate if running.

New section "Unattended resilience (when enabled)".

  • Keep-awake, three tiers from tonight's pmset -g log evidence. Lid open: caffeinate -dims in the background, PID recorded and killed at session end; this covers idle sleep only. Lid closed on AC: the Mac must never deep-sleep with the lid closed on AC so remote control and the panes keep running. caffeinate cannot prevent clamshell sleep without an external display, so the mechanism is the AC-profile setting sudo pmset -c disablesleep 1 (-c scopes it to the charger profile, battery behaviour unchanged; verify with pmset -g | grep SleepDisabled). With it on, closing the lid keeps the machine fully awake. The orchestrator cannot sudo, so at startup it checks the setting, tells the user in one line to run ! sudo pmset -c disablesleep 1 in the chat if it is 0 (noting the revert), optionally offers a one-time NOPASSWD sudoers rule for /usr/bin/pmset so later sessions can apply it with sudo -n, and re-checks pmset -g batt and the setting after any wake to remind once. Battery in a bag: nothing keeps the Mac awake; Power Nap plus TCP keepalive give short dark wakes every 5 to 15 minutes, pane agents retry inside those windows, and the run resumes when Wi-Fi is in range. The orchestrator relies on that by keeping auto-resume idempotent and fast, checks pmset -g custom at startup and warns once if powernap or tcpkeepalive is 0 without changing them, and tells the user once if on battery that plugged in with the lid open is the only fully awake setup.
  • Auto-resume: on READY or IDLE for a pane the orchestrator dispatched, read runpane panels screen. Resume only when composer.hasUndeliveredText is false (missing field means report, not resume) and the last thing the agent printed is one of the death signatures (sleep mid-response, can't reach the API server, ENOTFOUND, agent stalled, terminated early due to an API error, retry attempts exhausted). Submit a resume message via runpane panels submit naming the failure and pointing at the earliest incomplete gate of the runpane-orchestrator lifecycle, then a carriage return via runpane panels input, then confirm via screen that the resume message is no longer held and the agent is working. If the resume message is still held, panels submit-composer once, then report rather than retry. The whole sequence runs in one pass so it fits inside a wake window.
  • Guardrails: never resume a BLOCKED pane, STUCK stays with the Liveness Contract's resubmit rule, never more than 3 resumes per pane in a rolling hour (report instead, count is in-memory), never resume a pane the orchestrator did not dispatch unless the user asked it to keep all panes moving, log every resume in the next message to the user, and a resume never authorizes merge, deploy, release, publishing, version changes, or destructive actions.
  • Watcher re-arm: the dead-watch rule is unchanged. A HEARTBEAT gap over 120s that ends with lines arriving on their own is a wake, not a dead watch: re-run runpane watch --self-test before trusting new lines and do not spend the re-arm. Each wake resets the re-arm allowance. A gap with nothing arriving while the Mac is awake is a dead watch.
  • When disabled, none of this applies and the Liveness Contract stays as is.

Two small edits outside the shared blocks: the skill's Liveness Contract key-lines list now includes BUSY (the watcher already emits it) and describes IDLE as the pane being quiet rather than nothing dispatched, and the guide's Initialize list is introduced as "Do these" since it mixes reads and actions.

Hard stops are untouched. The watcher in packages/runpane is untouched.

Upgrade path. writePaneChatGuide rewrites every target with fs.writeFile on every ensurePaneChatGuide, which runs at app boot and at every Pane Chat launch. There is no hash or version gate, so existing installs receive the new text on next launch. The bootstrap prompt text is unchanged, so PANE_CHAT_BOOTSTRAP_VERSION stays at 9. A Pane Chat panel that is already open keeps its in-context copy until its terminal restarts.

Type of Change

  • New feature (non-breaking change which adds functionality)

Testing

main/src/services/skillCacheManager.test.ts gains two tests:

  • one reads all five emitted files (guide, canonical skill, Codex mirror, Claude mirror, Cursor rule) and asserts the question, the default-yes rule, the section heading, every death signature, every cited command including the pmset ones, the guardrails and the wake rule are present in each, plus ordering: doctor and self-test precede the question in the guide; in the skill the section sits after the Liveness Contract and before Hard stops; the question appears once per file
  • one seeds stale content at all five target paths, calls ensurePaneChatGuide, and asserts each was overwritten with the new section

Ran locally on the final diff:

  • pnpm lint passes
  • pnpm typecheck passes
  • pnpm --filter main exec vitest run: 960 passed, 2 skipped

Also rendered the generator into an isolated PANE_DIR and read the resulting files by eye: backticks, the printf '\r' line and the echo $! PID capture render correctly.

Review notes: a Codex code-reviewer pass flagged that the guide's Initialize had no self-test step before the question; fixed. A Claude implementation-reviewer pass found no must-fix items; its polish items are applied. A zero-context cold read of the PR body and the rendered files then drove the wording fixes above: the unanswered-question case, BUSY missing from the key lines, the guide referencing a Liveness Contract it does not contain, the 120s gap meaning both wake and dead watch, STUCK versus auto-resume, signature matches inside file output, the missing composer-field fallback, and the open-ended retry ladder.

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have run pnpm typecheck and pnpm lint locally
  • I have tested the Electron app locally with pnpm electron-dev

Critical Areas Modified

None.

Additional Notes

The 3-per-hour resume cap and the set of dispatched panes live in the orchestrator's own memory and reset if Pane Chat restarts. That is as specified; a durable counter would need state outside the skill text.

https://claude.ai/code/session_0171TCXkRCPujMN7aff1LHuc

Both generated variants (runpane-orchestrator.md guide and the
pane-orchestrator SKILL.md mirrored to Codex, Claude and Cursor) now ask
one startup question after doctor and the watcher self-test, defaulting
to yes, and carry a shared "Unattended resilience (when enabled)"
section: caffeinate keep-awake with the battery/clamshell caveat,
auto-resume of orchestrator-dispatched panes whose turn died from a
sleep or network API error, guardrails (never BLOCKED panes, max 3
resumes per pane per hour, log every resume, hard stops unchanged), and
a watcher self-test re-run after a detected wake.

Files are rewritten unconditionally on every ensurePaneChatGuide, so
existing installs pick the text up on next launch; the bootstrap prompt
is unchanged so PANE_CHAT_BOOTSTRAP_VERSION stays at 9.

Claude-Session: https://claude.ai/code/session_0171TCXkRCPujMN7aff1LHuc
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T03:00:17.356601Z c3b64cb PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3b64cbe2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread main/src/services/skillCacheManager.ts Outdated
Comment on lines +118 to +120
- Resume only when the composer is empty (the screen payload reports
\`composer.hasUndeliveredText: false\`) and the tail shows a
sleep/network death signature, one of:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve drafts for agents without composer detection

For Claude and Cursor panels, detectPanelComposer in main/src/ipc/runpane.ts unconditionally returns hasUndeliveredText: false, so this guard passes even when a user has typed an unsent draft. On the next READY/IDLE event with a matching error still visible, panels submit appends the resume text and presses Enter, potentially submitting or corrupting the user's draft; exclude agents without reliable composer detection or add a real held-input check before auto-resuming.

Useful? React with 👍 / 👎.

Comment thread main/src/services/skillCacheManager.ts Outdated
Comment on lines +116 to +120
- On a READY or IDLE line for a pane you dispatched, read
\`runpane panels screen --panel <panel-id> --limit 80 --json\`.
- Resume only when the composer is empty (the screen payload reports
\`composer.hasUndeliveredText: false\`) and the tail shows a
sleep/network death signature, one of:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require a fresh failure before auto-resuming

This searches an arbitrary 80-line current-screen snapshot after every READY or recurring IDLE event without tying the signature to the turn that just ended. After a pane successfully recovers, the earlier API error can remain visible above a short success response, causing the orchestrator to resume an already-completed pane repeatedly—up to three times per hour and again in later hours. Correlate the signature with the newest terminal outcome or record that the specific failure has already been handled.

Useful? React with 👍 / 👎.

Comment thread main/src/services/skillCacheManager.ts Outdated
Comment on lines +106 to +110
- Start \`caffeinate -dims\` in the background for the session and
record its PID. Kill that PID at session end.
- caffeinate cannot stop clamshell sleep on battery. Run
\`pmset -g batt\` once; if it reports battery power, tell the user
once to plug in and keep the lid open.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate macOS keep-awake commands by platform

When resilience defaults to enabled on supported Linux or Windows installations, the generated instructions still require caffeinate and pmset, which are macOS-only commands; initialization therefore produces command-not-found failures and provides no keep-awake behavior. Add an explicit Darwin check and either skip this subsection or use a supported platform-specific mechanism elsewhere.

AGENTS.md reference: AGENTS.md:L13-L13

Useful? React with 👍 / 👎.

Comment thread main/src/services/skillCacheManager.ts Outdated
Comment on lines +133 to +134
- Then send a carriage return, because submit alone often only pastes:
\`printf '\\r' | runpane panels input --panel <panel-id> --input-file - --yes --json\`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not press Enter after panels submit

The checked-in RunPane contract at contracts/runpane/contract.json:6247-6250 states that panels submit already stages text, sends the appropriate submit sequence, and verifies startup; sending another carriage return after it succeeds injects an extra keystroke into the now-running agent. The same contract's notes at lines 6297-6300 explicitly prohibit following this atomic command with another composer submission, so inspect the returned ok/blocked state instead of unconditionally pressing Enter.

AGENTS.md reference: AGENTS.md:L86-L86

Useful? React with 👍 / 👎.

Keep-awake now names three tiers from pmset -g log evidence: caffeinate
covers lid-open idle sleep only; lid closed on AC needs
`sudo pmset -c disablesleep 1`, which the orchestrator prints once for
the user and continues without; battery in a bag relies on Power Nap
plus TCP keepalive dark wakes, so auto-resume must be idempotent and
fast, and startup checks `pmset -g custom` for powernap and
tcpkeepalive without changing them.

Cold-read fixes: the default-yes rule no longer waits for an answer and
a later "no" kills caffeinate; BUSY joins the Liveness Contract key
lines and IDLE is described per pane; the shared section points at the
skill's Liveness Contract instead of assuming one in the guide; a
HEARTBEAT gap that ends with queued lines is a wake, not a dead watch,
and does not spend the re-arm; STUCK stays with the resubmit rule;
signatures inside file or tool output do not count; a missing composer
field means report, not resume; the retry ladder ends after one
submit-composer.

Claude-Session: https://claude.ai/code/session_0171TCXkRCPujMN7aff1LHuc
On AC the Mac must never deep-sleep with the lid closed, so remote
control and the panes keep running. Tier 1 now names the AC-profile
setting `sudo pmset -c disablesleep 1`, verified with
`pmset -g | grep SleepDisabled`, and gives the orchestrator a four-step
startup routine: check the setting; if 0, tell the user to run
`! sudo pmset -c disablesleep 1` in the chat and note the revert;
optionally offer the one-time NOPASSWD rule for /usr/bin/pmset so later
sessions can apply it with `sudo -n`; re-check battery and the setting
after any wake and remind once. Tier 2 (battery dark wakes) unchanged.

Claude-Session: https://claude.ai/code/session_0171TCXkRCPujMN7aff1LHuc
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