Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ff5845c
fix(session): clear transient probe errors and stop dead probe-error …
AlekShu Oct 14, 2026
6a60147
fix(session): gate stopped wake relaunch by admission with bounded da…
AlekShu Oct 14, 2026
d17d197
fix(session): report daily wake occurrence on admitted retry
AlekShu Oct 14, 2026
8457032
test(session): pin probe heal and wake admission boundaries
AlekShu Oct 14, 2026
471182a
fix(session): defer one wake record when its admission check throws
AlekShu Oct 14, 2026
c27db4d
fix(session): label admission check failures on deferred wakes
AlekShu Oct 14, 2026
a141af8
fix(session): recover historical process probe errors
AlekShu Oct 16, 2026
51ab316
Merge commit '21e579631fdd85e9629959d657e4cf3e10826bd0' into feature/…
AlekShu Oct 16, 2026
a010f65
fix(session): bind probe recovery to observed runtime
AlekShu Oct 16, 2026
9743b7f
docs(session): reference scheduled wake admission
AlekShu Oct 16, 2026
bde05a0
fix(session): guard deferred recovery observations
AlekShu Oct 16, 2026
d9a3cb9
fix(session): fence rejected orphan observations
AlekShu Oct 16, 2026
c31d594
fix(session): admit exact legacy diagnostics through probe recovery
AlekShu Oct 16, 2026
34b373b
fix(session): serialize daily denial with wake claims
AlekShu Oct 16, 2026
89ce30f
Merge remote-tracking branch 'origin/main' into feature/error-session…
AlekShu Oct 20, 2026
8ab9495
fix(session): skip runtime reconcile for unpersisted records
AlekShu Oct 20, 2026
d6e3453
fix(session): read final usage for boot-healed probe errors
AlekShu Oct 20, 2026
3aa7940
fix(session): integrate main recovery with scheduled wake admission
AlekShu Nov 6, 2026
edc81a8
test(runtime): synchronize isolated fixture readiness
AlekShu Nov 6, 2026
f3c10de
fix(session): preserve reboot siblings and reserve scheduled wake adm…
AlekShu Nov 6, 2026
0a2d488
chore(merge): integrate main usage reporting
AlekShu Nov 6, 2026
aa5920a
fix(session): omit absent wake admission reservations
AlekShu Nov 6, 2026
8ea306c
fix(session): hand wake reservations to counted warmup
AlekShu Nov 6, 2026
09f9d0a
fix(session): retain wake slot throughout pending launch
AlekShu Nov 6, 2026
7d91e47
fix(session): reconcile boot before background checks
AlekShu Nov 6, 2026
acfa9d0
test(session): use valid external lifecycle action
AlekShu Nov 6, 2026
aeb62f7
test(server): align startup events with boot reconciliation
AlekShu Nov 6, 2026
cfd827d
test(runtime): format startup event test
AlekShu Nov 6, 2026
825941e
chore(merge): integrate fresh main
AlekShu Nov 7, 2026
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
1 change: 1 addition & 0 deletions .agents/skills/spur/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ INTERFACES
Agent launch identity contract: `docs/commands.md#session-tools-and-environment`.
Manual-wake response fields: `docs/daemon-api.md`.
Automatic reminder limits: `docs/configuration.md#automatic-reminders`.
Scheduled-wake admission: `docs/configuration.md#admission-control`.
Edit scheduled jobs through footer Cron viewer or MCP; config behavior: docs/configuration.md; operations: docs/daemon-api.md.
Spur ToDo: ledger starts empty, no code path seeds an item; the agent adds one item per step, before the step, and resolves it after. Empty or open/held work refuses an agent's own completion, self-destruct, and handoff — `todo_ledger_empty`/`todo_open_work` (409); a human `complete`/`handoff` from the CLI or UI is never blocked. Contract: `docs/commands.md#todo`.
Telegram: bot DM for commands/spawn; agent communication in assigned forum topics; completion deletes owned topics. Source identity: `docs/commands.md#source`; attachments/binding: `docs/configuration.md#telegram-binding`; sends: `docs/commands.md#source-reply`.
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/spur/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ INTERFACES
Agent launch identity contract: `docs/commands.md#session-tools-and-environment`.
Manual-wake response fields: `docs/daemon-api.md`.
Automatic reminder limits: `docs/configuration.md#automatic-reminders`.
Scheduled-wake admission: `docs/configuration.md#admission-control`.
Edit scheduled jobs through footer Cron viewer or MCP; config behavior: docs/configuration.md; operations: docs/daemon-api.md.
Spur ToDo: ledger starts empty, no code path seeds an item; the agent adds one item per step, before the step, and resolves it after. Empty or open/held work refuses an agent's own completion, self-destruct, and handoff — `todo_ledger_empty`/`todo_open_work` (409); a human `complete`/`handoff` from the CLI or UI is never blocked. Contract: `docs/commands.md#todo`.
Telegram: bot DM for commands/spawn; agent communication in assigned forum topics; completion deletes owned topics. Source identity: `docs/commands.md#source`; attachments/binding: `docs/configuration.md#telegram-binding`; sends: `docs/commands.md#source-reply`.
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Type/constraint/default per key; full validation source `v2/src/config.ts`.

## Admission control

Cap limits concurrent live sessions at spawn, restore, and the wake of a stale-parked or memory-shed session. Over cap: `429` naming stop candidates. Reporting route: [`GET /headroom`](daemon-api.md). CLI: [`spur doctor`](commands.md#doctor). Schema `v2/src/config.ts`, enforcement `v2/src/session-service.ts`.
Cap limits concurrent live sessions at spawn, restore, and the wake of a stale-parked or memory-shed session (including one-shot wakes and relaunching sends). Ordinary stopped/paused interval/daily relaunches: background gated, manual ungated. Over cap: `429` naming stop candidates. Reporting route: [`GET /headroom`](daemon-api.md). CLI: [`spur doctor`](commands.md#doctor). Schema `v2/src/config.ts`, enforcement `v2/src/session-service.ts`.

`stopReason: "runtime_lost"`: confirmed runtime loss becomes restorable `stopped`, retains error evidence, releases capacity; unknown probes retain capacity (`v2/src/session-service.ts`).

Expand Down Expand Up @@ -185,7 +185,7 @@ Event names by source:

- Webhook request: `POST <path>` with `Authorization: Bearer <secret>` and `Content-Type: application/json`; runtime behavior and limits: `v2/src/event-sources/webhook.ts`.

Other event names (see source file for trigger conditions): `trigger.spawn.suppressed`, `source.poll.disabled`, `source.poll.enabled`, `source.poll_cycle.error`, `source.telegram.{callback_dropped,choice_rejected}`, `gh.poll_cycle`, `gh.usage`, `gh.poll_budget_paused`, `session.message.{sent,delivery_recovered,delivery_failed,requeued,submit_unconfirmed,queued_ahead,queue_removed,stalled}`, `session.spawn.{orphan_stopped,launch_unconfirmed,launch_submitted}`, `auto-ping.{muted,unmuted,migrated}`, `session.todo.{nudge_failed,nudge_disabled,nudge_exhausted}`, `session.{complete,pause,self_destruct,desk_complete,handoff}.{completed,failed}`, `session.wake.{failed,daily_failed,interval_failed,sent,daily_sent,interval_sent,interval_cancelled,daily_cancelled,suppressed}`, `session.wake.token_budget_blocked`, `session.token_budget.exhausted`, `session.token_budget.unenforced`, `session.token_budget.teardown_failed`, `session.attention_monitor.{failed,session_failed,slow}`, `session.runtime.{probe_unresponsive,pane_child_fallback}`, `session.agent_process.{pane_pid_unreadable,capture_unavailable,capture_blind,survivors}`, `session.{handoff,respawn}.startup_attachment_missing`, `session.server_error.reactivation_exhausted`, `session.sidecar.{start_noop,start_rejected,launch_reap_skipped,owner_missing}`, `session.list.enrich_failed`, `session.submit.{timeout,confirmed,released,failure_dismissed}`, `session.subscription.{delivery_failed,spawn_failed}`, `session.recover.context_unconfirmed`, `session.record.invalid`, `trigger.send.{failed,suppressed,suppressed_admission,suppressed_launch_pending}`, `host.disk.low`, `daemon.admission.startup`, `daemon.registry.{count,warning}`, `daemon.auto_update.{started,failed,retry,skipped,suppressed,paused,config_invalid,disarm_failed}`, `daemon.deploy_switch.{started,rejected}`, `cli.update.{started,rolled_back,abandoned}`.
Other event names (see source file for trigger conditions): `trigger.spawn.suppressed`, `source.poll.disabled`, `source.poll.enabled`, `source.poll_cycle.error`, `source.telegram.{callback_dropped,choice_rejected}`, `gh.poll_cycle`, `gh.usage`, `gh.poll_budget_paused`, `session.message.{sent,delivery_recovered,delivery_failed,requeued,submit_unconfirmed,queued_ahead,queue_removed,stalled}`, `session.spawn.{orphan_stopped,launch_unconfirmed,launch_submitted}`, `auto-ping.{muted,unmuted,migrated}`, `session.todo.{nudge_failed,nudge_disabled,nudge_exhausted}`, `session.{complete,pause,self_destruct,desk_complete,handoff}.{completed,failed}`, `session.wake.{failed,daily_failed,daily_deferred,daily_exhausted,interval_failed,interval_deferred,sent,daily_sent,interval_sent,interval_cancelled,daily_cancelled,suppressed}`, `session.wake.token_budget_blocked`, `session.token_budget.exhausted`, `session.token_budget.unenforced`, `session.token_budget.teardown_failed`, `session.attention_monitor.{failed,session_failed,slow}`, `session.runtime.{probe_unresponsive,pane_child_fallback}`, `session.agent_process.{pane_pid_unreadable,capture_unavailable,capture_blind,survivors}`, `session.{handoff,respawn}.startup_attachment_missing`, `session.server_error.reactivation_exhausted`, `session.sidecar.{start_noop,start_rejected,launch_reap_skipped,owner_missing}`, `session.list.enrich_failed`, `session.submit.{timeout,confirmed,released,failure_dismissed}`, `session.subscription.{delivery_failed,spawn_failed}`, `session.recover.context_unconfirmed`, `session.record.invalid`, `trigger.send.{failed,suppressed,suppressed_admission,suppressed_launch_pending}`, `host.disk.low`, `daemon.admission.startup`, `daemon.registry.{count,warning}`, `daemon.auto_update.{started,failed,retry,skipped,suppressed,paused,config_invalid,disarm_failed}`, `daemon.deploy_switch.{started,rejected}`, `cli.update.{started,rolled_back,abandoned}`.

## Auto update

Expand Down
2 changes: 1 addition & 1 deletion docs/daemon-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
- `POST /sessions/background` — `409 spawn_in_progress|spawn_committed|spawn_failed` body `{ code, sessionId, error }` when `preflightBatchId` is already claimed; `sessionId` is the claiming session; `v2/src/session-service.ts`.
- `POST /shepherd/spawn`
- `POST /sidecars/sweep` — body `{ reap?: boolean }`, default `false`. Always `200`. Response `{ supported, leaked, reaped }`: `supported` is `false` when the process table or procfs is unreadable, in which case `leaked` and `reaped` are both `[]`. Each `leaked` row carries `kind: "worktree-tree" | "orphan-daemon"`; a `worktree-tree` row's `reapable` reflects proven Spur provenance and carries `rootPid`, `pgid`, `ageSeconds`, `worktreePath`, `args`, `sidecarName`, `tree` (descendant pids, root first), `treeRssKb`; an `orphan-daemon` row is always `reapable: false` and carries `configPath`/`cliEntryPath` instead of a `sidecarName`, plus `port: number | null` and `liveness: "serving" | "not-serving" | "unknown"`. `reap: true` signals only `reapable` rows — never an `orphan-daemon` row, serving or not — and populates `reaped`, one entry per `reapable` leaked tree it signaled: `{ sessionName, panePid, survivors, blindKill }` — `survivors` is the pids still alive after the SIGKILL confirmation window, and `blindKill` is `true` when tmux was killed with no verified process-tree signal (no pane pid, or an unusable snapshot, and no identity fallback confirmed a reap either) — a `survivors: []` alongside `blindKill: true` is not proof anything died, only that nothing was checked. See [Sidecars](commands.md#sidecars)
- `GET /sessions/:id` — current row, `sidecars[].url` ready endpoint, `sidecars[].ports[].listening` host TCP listener true|false|null; Cursor `queuedMessageTyped.ackBaseline.deliveryId` optional UUIDv4, correlated `file:null` requires `size:0` and valid ID, malformed IDs drop baseline, legacy string-file floors retained; optional read-only `intervalWake.dispatchClaimId`: opaque correlation, absent on legacy records, ignored on input; `v2/src/types.ts`, `v2/src/metadata.ts`; ephemeral `lifecycle` receipt; `v2/src/session-lifecycle.ts`.
- `GET /sessions/:id` — current row, `sidecars[].url` ready endpoint, `sidecars[].ports[].listening` host TCP listener true|false|null; Cursor `queuedMessageTyped.ackBaseline.deliveryId` optional UUIDv4, correlated `file:null` requires `size:0` and valid ID, malformed IDs drop baseline, legacy string-file floors retained; optional read-only `intervalWake.dispatchClaimId`: opaque correlation, absent on legacy records, ignored on input; optional `dailyWake.admissionDeferredOccurrence`: persisted retry occurrence; `v2/src/types.ts`, `v2/src/metadata.ts`; ephemeral `lifecycle` receipt; `v2/src/session-lifecycle.ts`.
- `GET /sessions/:id/slash-commands`
- `GET /sessions/:id/conversation` — optional `view=messages&limit=<1..100>` (default 50): bounded visible-message page; `v2/src/server.ts`, `v2/src/conversation-page.ts`; omit view for legacy response.
- `GET /sessions/:id/user-actions`
Expand Down
5 changes: 3 additions & 2 deletions v2/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,6 @@ export async function startServer(
setGhEventSink(cfg.dataDir);
};
applyLogConfigs(service.config);
service.startBackgroundLoops();
const bus = new EventBus();
let triggers: TriggerGroupController | null = null;
let sources: Awaited<ReturnType<typeof startConfiguredSources>> | null = null;
Expand Down Expand Up @@ -2672,7 +2671,7 @@ export async function startServer(
throw error;
}

let driftedSessions: { id: string; project: string }[] = [];
let driftedSessions: Parameters<SessionService["restoreRebootedSessions"]>[0] = [];
try {
const {
scanned,
Expand All @@ -2694,6 +2693,8 @@ export async function startServer(
});
}

service.startBackgroundLoops();

try {
await startAutomation();
} catch (error) {
Expand Down
Loading
Loading