Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -234,6 +234,13 @@ and versions follow [Semantic Versioning](https://semver.org/).
`--keep-live`.
- 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 @@ -104,6 +104,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