Skip to content

feat(core,kde): opt-in session keepalive to open the 5-hour window early - #11

Open
danielbanariba wants to merge 2 commits into
AltairInglorious:mainfrom
danielbanariba:feat/session-keepalive
Open

danielbanariba wants to merge 2 commits into
AltairInglorious:mainfrom
danielbanariba:feat/session-keepalive

Conversation

@danielbanariba

@danielbanariba danielbanariba commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #10

Summary

  • New opt-in shared/core/keepalive.js: shouldKeepalive() decides whether to ping (enabled, fresh poll result, no active 5-hour window, weekly quota not exhausted, 30-minute cooldown), buildShellCommand() wraps the user's command with the PATH prefix a plasmashell-style environment needs plus a 120s timeout, createKeepalive() wires those into a controller that never throws out of the scheduler's poll loop.
  • shared/core/scheduler.js calls the controller once per Claude poll result, right after applyProviderResult/backoffManager.recordResult. With no controller injected (the default, and always true for the GNOME extension) the scheduler behaves exactly as before.
  • One warning line on the Claude card (shared/ui/render.js) when the last attempt failed, e.g. "Keepalive failed (exit 127)", so a broken PATH or command is visible instead of silent.
  • KDE: keepaliveEnabled (bool, default false) and keepaliveCommand (string, defaults to the ping command below) in both main.xmls, a checkbox + advanced text field in both configGeneral.qmls, and runShell() added to ExecReader.qml — reusing the same Plasma "executable" DataSource the widget already uses to read the credentials file. Plasma 5 and 6 trees kept in parity.
  • README subsection explaining the mechanics and token cost; keepalive.js added to the CLAUDE.md architecture list.

Why

The 5-hour session window opens at the first message and closes exactly 5 hours later. If the machine is on well before the first real message, the reset lands later than it needs to. A tiny ping at boot (or whenever no window is active) opens the window earlier, which only ever moves the reset earlier — unused quota does not carry over, so this cannot hurt. Default keepaliveCommand:

claude -p ok --model haiku --strict-mcp-config --setting-sources "" --no-session-persistence --tools "" --system-prompt "Reply with the single word: ok"

~240 input / ~40 output tokens per ping, at most a few per day. plasmashell does not see the user's shell PATH (confirmed via systemctl --user show-environment and /proc/<pid>/environ on the machine this was built on — both show only /usr/local/bin:/usr/bin), so buildShellCommand() prepends $HOME/.local/bin and $HOME/.claude/local itself rather than relying on a login shell.

Changes

File Change
shared/core/keepalive.js New: shouldKeepalive, buildShellCommand, createKeepalive
shared/core/scheduler.js Optional keepalive dependency; hook called after each Claude poll result; status exposed on the summary object
shared/ui/render.js toKeepaliveWarningText(); one warning line on the Claude card
shared/index.mjs Re-export the new core surface
kde/shared/code/app.mjs createApp() accepts isEnabled/getCommand/runCommand and builds the keepalive controller
kde/plasma{5,6}/contents/config/main.xml keepaliveEnabled, keepaliveCommand entries
kde/plasma{5,6}/contents/ui/configGeneral.qml Checkbox + advanced command field, with the PATH caveat spelled out
kde/plasma{5,6}/contents/ui/ExecReader.qml runShell(cmd) — runs an already-composed command through the existing DataSource, resolves {exitCode, stderr}
kde/plasma{5,6}/contents/ui/main.qml Wires Plasmoid.configuration.keepalive* and reader.runShell into App.createApp()
README.md, CLAUDE.md Document the feature
test/unit/keepalive.test.js, additions to scheduler.test.js and ui-render.test.js Full coverage, written before the implementation

Test plan

  • New test/unit/keepalive.test.js covers: enabled/disabled, every fresh vs. stale status code, an active vs. expired vs. missing/unparsable sessionResetsAtIso (including the grace period boundary), weekly-exhausted, the cooldown, buildShellCommand()'s exact output, and createKeepalive()'s controller behavior (records the attempt time before the runner resolves, never throws when the runner rejects or is missing, only acts on the claude provider).
  • scheduler.test.js additions: the hook fires with the mapped {code, data} shape after a Claude poll, and is a silent no-op when no keepalive option is passed.
  • ui-render.test.js additions: the warning line appears only after a failed attempt, and is absent otherwise.
  • Full suite green: 106 passed, 0 failed (this branch merged locally on top of the already-installed rate-limit and backoff/stalled-request fixes; on its own, cut clean from main, it is 96 passed / 1 pre-existing, unrelated failure — bun:test's vi.advanceTimersByTime shim, present on main before this change).
  • KDE bundle rebuilt for both plasma5 and plasma6 via scripts/build/bundle-kde.sh; confirmed by rg that the compiled bundle contains the new identifiers, and that esbuild's ES2015 target correctly downlevels every ?./??/async/await this code uses (zero matches for ?./?? in the compiled output).
  • Verified how a missing/empty five_hour block is classified by the existing Claude provider (PARTIAL_DATA when the block is entirely absent, OK with sessionResetsAtIso: null when present but zeroed) and confirmed by test that the keepalive trigger fires correctly in both shapes.

Notes for review

  • The scheduler hook receives the state's mapped {code, data} (post-applyProviderResult), not the provider's raw {ok, data, error} — this avoids re-deriving the OK/PARTIAL_DATA/etc. classification a second time inside keepalive.js. Happy to change the shape if you'd rather it stayed closer to the raw provider result.
  • GNOME parity is intentionally out of scope for this PR — extension/ is untouched. Opening a follow-up issue for it if this lands.
  • The keepalive command is entirely user-configured (their own settings field, run as their own user), the same trust boundary as the widget already crossing into a shell via ExecReader.qml's existing readFile/resolveHome.

… scheduler hook

Add shared/core/keepalive.js: buildShellCommand() wraps a user command with
the PATH prefix needed under plasmashell (which does not see the user's
shell PATH) and a timeout guard; shouldKeepalive() decides whether to ping
based on enabled state, freshness of the poll result (OK/PARTIAL_DATA only,
never stale/retained data), whether a 5-hour session window is already
active, remaining weekly quota, and a cooldown; createKeepalive() wires
those into a controller with onProviderResult()/getStatus() that never
throws out of the scheduler's poll loop.

Hook the controller into shared/core/scheduler.js right after a provider
result is applied, and expose its status on the summary object so
shared/ui/render.js can render one warning line on the Claude card when the
last attempt failed (e.g. "Keepalive failed (exit 127)"). The hook is a
no-op when no keepalive controller is injected, so the GNOME extension
(which never wires one) is unaffected.
Add keepaliveEnabled (Bool, default false) and keepaliveCommand (String,
defaulting to the verified `claude -p ok --model haiku ...` ping) to both
Plasma variants' main.xml, and a matching CheckBox + advanced TextField in
configGeneral.qml explaining the mechanics and the PATH caveat.

Extend ExecReader.qml with runShell(cmd), reusing the existing "executable"
Plasma DataSource (already used for reading credentials) to run an
already-composed shell command asynchronously and resolve {exitCode,
stderr}. Wire main.qml to pass isEnabled/getCommand/runCommand -- backed by
Plasmoid.configuration and reader.runShell -- into App.createApp(), and
extend kde/shared/code/app.mjs to build a keepalive controller from those
deps and inject it into the scheduler. Both Plasma 5 and Plasma 6 trees are
kept in parity.

Document the feature (mechanics, token cost, PATH note, GNOME follow-up) in
the README and list keepalive.js in CLAUDE.md's architecture overview.
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.

Keep the Claude 5-hour session window open (opt-in)

1 participant