Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ and versions follow [Semantic Versioning](https://semver.org/).
### Fixed
- Chats using Claude through your own Claude Code now show the context meter and the "getting long" warning at
85%: the context length is read from the matching Anthropic model, which LiteLLM knows, instead of nothing.
- **Background work on a laptop that stays on battery:** Sentient's own upkeep (memory, what it learns about you,
dreams, suggestions, skill review, chat titles) used to wait for the charger, so a laptop used all day on battery
never ran it. It now runs on battery too, gently: while the battery is above 40%, after you have not chatted for
five minutes, in short bursts at most every 30 minutes, and never held more than two hours. At 15% or below it
waits. Tasks you scheduled always run on time, whatever the battery, and so does Dream now unless the battery is
low. The title bar says "Waiting: on battery" (or
"Waiting: battery low") while upkeep waits, and every limit is in Settings > Models > Sharing your local model.
- Messages and memories saved at the same moment now always keep their order, and the daily upkeep of MEMORY.md and
USER.md no longer misses a memory saved while it was reading. Summarizing a long chat no longer skips messages
saved at the same moment as the last summary.
Expand Down
29 changes: 20 additions & 9 deletions desktop/src/components/shell/ModelBusy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* #149 One local model job at a time: a small title bar indicator, shown while the local model works on something
* other than your chat, while something waits for it, or while background work is paused on battery.
* other than your chat, while something waits for it, or while Sentient's own upkeep waits on battery (#296).
*/
import { Tooltip, StatusDot } from '@/components/ui'
import { useModelBusy } from '@/hooks/models'
Expand All @@ -19,22 +19,30 @@ const JOB: Record<ModelJobKind, { short: string; doing: string }> = {
background: { short: 'background work', doing: 'doing background work' }
}

const BATTERY_TIP: Record<'battery' | 'battery_low', string> = {
battery:
"Sentient's own upkeep (memory, suggestions, chat titles) is waiting because this computer is on battery. " +
'It runs now and then once the battery is above your limit and you have stopped chatting for a few minutes. ' +
'It never waits longer than the limit in Settings > Models. Your chats and the tasks you scheduled run on time.',
battery_low:
"Sentient's own upkeep (memory, suggestions, chat titles) is waiting because the battery is low. " +
'It never waits longer than the limit in Settings > Models. Your chats and the tasks you scheduled still run on time.'
}

export function modelBusyText(s: ModelBusy): { label: string; detail: string | null; tip: string } | null {
const battery = s.deferred_reason === 'battery'
// #296: on battery only Sentient's own upkeep waits, never your chats or tasks; say so honestly
const battery = s.deferred_reason === 'battery' || s.deferred_reason === 'battery_low' ? s.deferred_reason : null
if (!(s.busy && (s.job !== 'chat' || s.waiting > 0)) && !battery) return null
const lines: string[] = []
if (s.busy && s.job) {
const name = modelShortName(s.model) || 'Your local model'
lines.push(`${name} is ${JOB[s.job]?.doing ?? 'busy'}. It does one job at a time so it stays fast.`)
}
if (s.waiting > 0) lines.push(`${s.waiting} more waiting. Your chats always go first.`)
if (battery) lines.push('Background work waits until you plug in. You can change this in Settings > Models.')
if (battery) lines.push(BATTERY_TIP[battery])
else if (s.deferred > 0) lines.push('Background work waits until you have stopped chatting for a moment.')
return {
label: s.busy ? 'Model busy' : 'Paused on battery',
detail: s.busy && s.job ? JOB[s.job]?.short ?? null : null,
tip: lines.join(' ')
}
if (s.busy) return { label: 'Model busy', detail: s.job ? JOB[s.job]?.short ?? null : null, tip: lines.join(' ') }
return { label: 'Waiting', detail: battery === 'battery_low' ? 'battery low' : 'on battery', tip: lines.join(' ') }
}

export function ModelBusyIndicator() {
Expand All @@ -50,7 +58,10 @@ export function ModelBusyIndicator() {
className="no-drag flex h-6.5 max-w-60 items-center gap-1.5 rounded-full border border-border bg-surface/60 px-2.5 text-xs text-fg-muted"
>
<StatusDot tone={data.busy ? 'info' : 'warning'} pulse={data.busy} />
<span className="shrink-0 font-medium text-fg">{text.label}</span>
<span className="shrink-0 font-medium text-fg">
{text.label}
{!data.busy && text.detail ? ':' : ''}
</span>
{text.detail && <span className="truncate text-fg-subtle">{text.detail}</span>}
{data.waiting > 0 && (
<span className="shrink-0 rounded-full bg-active px-1.5 text-[10px] font-medium text-fg-muted">+{data.waiting}</span>
Expand Down
7 changes: 6 additions & 1 deletion desktop/src/features/settings/SchemaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export const LABELS: Record<string, FieldLabel> = {
// models (#149)
'models.local_queue': { label: 'One job at a time, your chats first' },
'models.background_quiet_s': { label: 'Background work waits after you chat', unit: 's' },
'models.background_on_battery': { label: 'Background work on battery' },
'models.background_on_battery': { label: 'Lift the battery limits for upkeep' },
'models.battery_min_percent': { label: 'On battery, upkeep needs more than', unit: '%' },
'models.battery_critical_percent': { label: 'Battery low: upkeep waits at or below', unit: '%' },
'models.battery_idle_s': { label: 'On battery, upkeep waits after you chat', unit: 's' },
'models.battery_upkeep_every_s': { label: 'On battery, upkeep runs at most every', unit: 's' },
'models.battery_max_wait_s': { label: 'Upkeep never waits longer than', unit: 's' },
// tasks
'backups.schedule': { label: 'Back up automatically' },
'backups.keep': { label: 'Automatic backups to keep' },
Expand Down
11 changes: 10 additions & 1 deletion desktop/src/features/settings/sections/Models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,16 @@ const ROLE_ICON: Record<RoleName, Icon> = {
}

/** #149: one local model job at a time, your chats first. */
const MODEL_QUEUE_KEYS = ['models.local_queue', 'models.background_quiet_s', 'models.background_on_battery']
const MODEL_QUEUE_KEYS = [
'models.local_queue',
'models.background_quiet_s',
'models.background_on_battery',
'models.battery_min_percent',
'models.battery_critical_percent',
'models.battery_idle_s',
'models.battery_upkeep_every_s',
'models.battery_max_wait_s'
]

export function ModelsSection({ query }: SectionProps) {
const { config } = useConfigEditor()
Expand Down
9 changes: 8 additions & 1 deletion desktop/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export interface ModelBusy {
waiting: number
/** Background jobs held back while you chat or on battery. */
deferred: number
deferred_reason: 'chat' | 'battery' | null
/** #296: on battery only Sentient's own upkeep waits; `battery_low` at or below `battery_critical_percent`. */
deferred_reason: 'chat' | 'battery' | 'battery_low' | null
}

export interface StopResult extends StopState {
Expand Down Expand Up @@ -139,6 +140,12 @@ export interface ModelsConfig {
local_queue?: boolean
background_quiet_s?: number
background_on_battery?: boolean
/** #296: how upkeep behaves on battery. Tasks always run on time. */
battery_min_percent?: number
battery_critical_percent?: number
battery_idle_s?: number
battery_upkeep_every_s?: number
battery_max_wait_s?: number
}

export interface GatewayConfig {
Expand Down
21 changes: 17 additions & 4 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,23 @@ accept or loosen a rule.
`memory` (memory notes after a reply, compression, the user model, dreams), `skills`, `titles`, `background`. Waiting
calls go in that order, then first come first served; a running call is never cut off. `waiting` counts calls that go
as soon as the model is free; `deferred` counts background calls (`task` and below) held back although it may be
free, with `deferred_reason`: `"chat"` while a chat reply runs or ended less than `models.background_quiet_s`
(default 30) seconds ago, `"battery"` while the computer runs on battery (Windows, macOS and Linux power status) and
`models.background_on_battery` is off (the default). Work a running chat reply waits on is never held back, and a
reply that has not used the model for 60 seconds stops holding background work back, so the queue can't deadlock.
free, with `deferred_reason` (the most telling one when several apply: `battery_low`, then `battery`, then
`chat`): `"chat"` while a chat reply runs or ended less than `models.background_quiet_s` (default 30) seconds ago;
`"battery"` or `"battery_low"` while Sentient's own upkeep waits on battery (#296). Battery holds apply only to
upkeep (`suggestions`, `memory`, `skills`, `titles`, `background`); `task` calls run on time at any battery level.
On battery (Windows, macOS and Linux power status) upkeep goes only while the battery is above
`models.battery_min_percent` (default 40), you have not chatted for `models.battery_idle_s` (default 300), and in
bursts at most every `models.battery_upkeep_every_s` (default 1800; a burst stays open while upkeep keeps using the
model, so a pass of several calls finishes together); `models.background_on_battery` (default off) lifts these
three limits. At or below `models.battery_critical_percent` (default 15) upkeep is `"battery_low"` and waits even
with that switch on. Whatever the battery, no call waits longer than `models.battery_max_wait_s` (default 7200)
because of it; a call the cap lets go opens a burst too, so the rest of its pass follows at once (a burst lasts at
most 5 minutes). Upkeep a person starts from the window (`POST /api/memories/dreams/run`, Dream now) skips the usual
limits: on battery only `models.battery_critical_percent` (and the cap) holds it, as `"battery_low"`. When the cap
lets it go, the rest of it follows in a burst of its own; scheduled upkeep never rides along. Work a running chat reply waits on is never held back, and a reply that has not used the model for
60 seconds stops holding background work back, so the queue can't deadlock. For tests and debugging only, the
engine's `SENTIENT_DEBUG_BATTERY` environment variable fakes the reading: `ac` (plugged in), `battery` (on battery,
level unknown) or a percent from 0 to 100 (on battery at that level).
A stream holds the model until its last chunk; cancelling (Stop everything) frees it.
- `GET /api/models/claude-code` → `{enabled, installed, version, detail, models}`: Claude through the user's own
Claude Code (experimental, ADR 0022). `enabled` is `models.experimental_claude_code` (default false). `installed`
Expand Down
5 changes: 5 additions & 0 deletions docs/DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ When several people build and capture screenshots at once, take `desktop/.build.
.venv/Scripts/ruff.exe check sentient tests
```

To check how background work behaves on battery on a computer that is plugged in, start the engine with
`SENTIENT_DEBUG_BATTERY` set: `ac`, `battery`, or a percent such as `35` or `10` (tests and debugging only; the
engine logs a warning while it is set). The engine log then shows `model job held: ... (on battery ...)`,
`upkeep burst on battery` and `model job released: ...` lines.

No test touches the network; the LLM is a scripted, deterministic fake
(`tests/conftest.py`). Browser tests drive a hidden local Edge or Chrome against local pages and skip
when none can start; Docker sandbox tests skip when Docker is not running. The suite is large: on a busy
Expand Down
28 changes: 26 additions & 2 deletions sentient/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,32 @@ class ModelsConfig(BaseModel):
)
background_on_battery: bool = Field(
False,
description="Let background work use a local model while this computer runs on battery. When off, it waits "
"until you plug in. Your chats always run.",
description="Let Sentient's own upkeep (memory, suggestions, skill review, chat titles) use a local model on "
"battery as freely as when plugged in, except when the battery is low (below). When off, upkeep still runs on "
"battery, with the limits below. Your chats and the tasks you scheduled always run on time.",
)
battery_min_percent: int = Field(
40, ge=0, le=100,
description="On battery, Sentient's own upkeep uses a local model only while the battery is above this "
"percent. Your tasks run on time at any level.",
)
battery_critical_percent: int = Field(
15, ge=0, le=100,
description="At or below this battery percent, upkeep waits even when it is allowed on battery. Your tasks "
"still run.",
)
battery_idle_s: int = Field(
300, ge=0, le=7200,
description="On battery, upkeep waits until you have not chatted for this many seconds.",
)
battery_upkeep_every_s: int = Field(
1800, ge=0, le=86400,
description="On battery, upkeep runs in short bursts at most this often, in seconds. 0 lets it run "
"whenever the other limits allow.",
)
battery_max_wait_s: int = Field(
7200, ge=60, le=86400,
description="Upkeep never waits longer than this many seconds because of the battery, whatever its level.",
)


Expand Down
Loading
Loading