Skip to the content.

Long-running agent loops - fak as the loop kernel

Date: 2026-06-25.

This note is the missing complement to three existing docs:

The remaining problem is operational: an unattended loop is not just an agent prompt. It is a scheduled, resumable, observable, remotely steerable process that can run while nobody is watching. Today fak has many good pieces, but the shape is scattered across Windows Scheduled Tasks, launchd templates, watchdog scripts, Slack lab helpers, fak manage, fak serve, taskmgr, session images, and DOS witness gates.

The product line should be:

fak is the kernel for long-running agent loops. Cron, launchd, Task Scheduler, Slack, WhatsApp, GitHub, and phones are just interrupt sources. fak owns the capability floor, loop identity, run ledger, leases, witness, resume state, and notification discipline.

Status update (2026-06-28): the in-kernel RUNTIME + tick source landed. Rungs 1–2 below (the durable loopmgr ledger and the OS-scheduler adapters) drive every tick from OUTSIDE the kernel — loopmgr itself “schedules, spawns, notifies, authorizes nothing”. internal/bgloop + fak bgloop add the piece that was missing on this whole ladder: an in-kernel Supervisor that RUNS registered loops in their own goroutines on the fak serve lifecycle, so a loop progresses BECAUSE the kernel is up, with no external scheduler firing it. It contains panics/errors with capped backoff (a crashing loop never takes the kernel down), joins cleanly on shutdown, and is observable at GET /v1/fak/loops, via the fak_bgloop_* Prometheus family, and through fak bgloop status / fak bgloop demo (the offline witness). It folds into this note’s substrate without coupling — WithObserver pushes ticks into the loopmgr ledger (so an in-kernel loop also shows in fak loop status) and WithAdmit gates fires through loopmgr.Governor.Admit. This is the runtime + read-only observability complement to Rung 3 below; the authenticated POST /v1/fak/loops/{id}/fire|signal control bridge remains the next step. See CLAIMS.md (Gateway) and internal/bgloop/doc.go.

External signals

Current public products all point in the same direction:

The important read is not “build another cloud agent.” The field already has VMs, dashboards, IDEs, and phone apps. The unresolved layer is the one fak is already good at: what may run, where it may run, how it is admitted, how it is observed, how it proves done, and what state survives the next tick.

Current fak substrate

fak already has most primitives, but not one named loop-control surface.

Need Existing substrate Honest gap
Foreground guarded session fak manage -- <agent> launches a loopback gateway and child-only base URL Tied to one local child process; lifecycle ends with the child
Always-on gateway fak serve, deployment docs, auth, metrics, /debug/vars, audit journal Gateway is a front door, not yet a loop scheduler
Host schedulers Windows Scheduled Tasks installers, launchd plists, Mac keep-awake script, GCP dogfood plan Each is bespoke; no common loop record or run ledger
Fleet admission DOS lane leases, dos_arbitrate, issue-dispatch preflight and cooldowns Strong in the dispatch path, not exposed as a general loop admission API
Progress and resource view internal/taskmgr, fak task sample, gated GET /v1/fak/tasks Process-local only; not durable, cross-PID, or fleet-wide
Session control internal/session drive state, budgets, continuation ids, reset-on-budget A served-session primitive, not yet the outer loop’s scheduler state
Durable resume internal/sessionimage, internal/snapshot, recall core images Captures session state, not the loop’s schedule, owner, run count, and remote trigger history
Notifications tools/notify.ps1, Slack benchmark bridge, dispatch status docs Output sinks exist, but no normalized notification/ack contract
Node operation node-macos-a runbook, dogfood coverage, hardware matrix No node registry that says “this loop can run here, under this policy, with this gateway”

The missing abstraction: a Loop Record

One durable loop record should sit above taskmgr and session:

{
  "schema": "fak.loop.v1",
  "loop_id": "issue-dispatch/default",
  "owner": "fleet",
  "mode": "background",
  "trigger": {"kind": "schedule", "spec": "every 10m"},
  "target": {"kind": "local", "node": "workstation-a", "workspace": "C:/work/fak"},
  "policy": "examples/dev-agent-policy.json",
  "state": "armed",
  "next_fire_unix": 1782422400000000000,
  "last_fire_unix": 1782421800000000000,
  "runs": {
    "attempted": 411,
    "admitted": 392,
    "succeeded": 173,
    "witnessed": 167,
    "refused": 19
  },
  "last_run": {
    "run_id": "run_20260625_101500_abc123",
    "status": "witnessed_done",
    "reason": "",
    "evidence": [{"kind": "commit", "ref": "8469c56"}]
  },
  "notify": [{"sink": "windows_toast"}, {"sink": "slack:#fleet"}]
}

This is not a replacement for a task snapshot. taskmgr answers “what is this process doing right now?” A loop record answers “what recurring logical loop exists, when did it fire, where did it run, what happened, and who has been told?”

Foreground, background, and green threads

The OS analogy helps if it stays precise.

Agent loop concept OS analogy fak meaning
Foreground session foreground process A user-attached fak manage -- claude or CLI run. Terminal owns attention; exit ends the loop.
Background loop daemon/service A durable loop record with an owner, schedule, policy, budget, run ledger, and notification sinks. It can survive logout, host sleep, and a fresh process.
Loop tick timer interrupt A scheduler event from cron/launchd/Task Scheduler/K8s/GitHub/HTTP. It requests a run; fak still decides admission.
Agent green thread cooperative user-space thread A logical loop fiber multiplexed over scarce workers. It is cheap to keep as state, only binds an OS process/VM while executing a tick, and yields at turn/session boundaries.
Worker process/VM kernel thread / execution context The concrete child process, container, microVM, remote SSH session, or cloud task that runs one admitted slice.
Signal notification/control event Pause, drain, resume, speed up, reduce budget, request status, or request witness. Signals mutate loop/session state; they are not arbitrary shell commands.

“Green thread” is the right model for the long-running fleet: keep thousands of logical loops as small records, but run only the few admitted by leases, accounts, budgets, and node capacity. The scheduler is cooperative because the safe yield point is a fak boundary: before a run, between turns, at reset/drain, or after a witness check. It should never kill mid-tool-call unless the state is already corrupt and the kill is recorded as such.

What fak should own

1. Loop identity and durable run ledger

Every background loop needs an append-only, hash-chained ledger, similar in spirit to the guard audit journal, but at loop granularity:

This is how fak answers “how often did the background loop actually run?” without scraping OS scheduler logs or trusting the worker’s final sentence.

2. Trigger adapters, not trigger trust

Cron, launchd, Windows Scheduled Tasks, systemd timers, Kubernetes CronJobs, GitHub events, Slack, WhatsApp, phone actions, and HTTP /fire should all lower into the same control event:

{"event": "fire", "loop_id": "issue-dispatch/default", "source": "slack", "principal": "alice", "nonce": "..."}

The source authenticates the request, but does not decide the run. fak still checks:

That split is the fak-shaped part. Phone and chat are convenient control panes, not new trust roots.

3. Node registry and execution targets

Add one registry that says what each node can host:

{
  "node_id": "node-macos-a",
  "platform": "darwin/arm64",
  "scheduler": "launchd",
  "gateway_url": "http://127.0.0.1:8080",
  "labels": {"metal": "true", "always_on": "true"},
  "capacity": {"workers": 4, "interactive": 1},
  "heartbeat_unix": 1782421800000000000
}

Targets should be typed, not stringly:

fak should not become a VM provider. It should make the boundary travel to whichever target the operator already uses.

4. Notifications and acknowledgements

Notifications need a normalized contract:

Sinks can be Windows toast, macOS notification, Slack, WhatsApp Business/webhook, email, GitHub comment, A2A task event, or a local status file. Sinks are interchangeable because they do not carry authority by themselves. Authority is in the control event that comes back, authenticated and re-adjudicated.

5. Witnessed completion by default

Long-running loops are where self-report is most tempting and least useful. A run can finish in four distinct states:

This mirrors internal/taskmgr.WitnessRecord and the DOS discipline already used by issue dispatch. It keeps “green status” from meaning “the worker stopped talking.”

Proposed ladder

Rung 0 - name the product surface

Document fak loop as the umbrella for long-running loops:

This can be documentation-first. The point is to stop treating each watchdog script as a different product.

Rung 1 - durable loop ledger

Implement the loop record and JSONL ledger with a small stdlib-only package. First host: wrap existing issue-dispatch and dogfood loops without changing their behavior. Success means a loop can answer:

Rung 2 - OS scheduler adapters

Keep OS schedulers as thin drivers:

Each adapter should only fire a loop id and then record the fire result. All policy, leases, and witnesses live above it.

Rung 3 - control/notification bridge

Expose a network-safe control endpoint only after auth is strong enough:

Then bind Slack/phone/WhatsApp/GitHub to those routes. The order matters: a remote control surface before real auth is an anti-feature.

Rung 4 - node registry and remote targets

Add node heartbeats and typed execution targets. Start with local and remote SSH/Tailscale. Add container/microVM/cloud-task as adapters, not as fak-owned infrastructure.

Admission should be:

fire -> auth -> loop policy -> node eligibility -> lease/capacity -> spawn target -> witness -> notify

Rung 5 - cache-aware green-thread scheduler

Once logical loops are cheap records, schedule them with fak’s differentiators:

This is where fak’s security and reuse boundary become one operational advantage, not two separate features.

What not to build

First practical wedge

The first Rung 1 slice is shipped as of 2026-06-25:

Honest fence: this is not yet a scheduler, node registry, chat/phone bridge, or general notification plane. fak loop run wraps commands and the main Windows/Mac/GCP dogfood scheduler templates now call it, but fak still does not install or own cron/launchd/systemd generally. Only the issue-resolution dispatcher and progress/close proof ticks are wired as producers; worker-effect completion still belongs to the independent close/audit arm.

Remaining Rung 1 work:

  1. Reuse internal/taskmgr.WitnessRecord vocabulary for run completion.
  2. Reuse existing notification sinks only after the run row exists, so notifications are audit-backed.

This gives the operator the thing the current system lacks most: “what loops exist, when did they last actually run, what did they prove, and what should page me?”

Success metrics

The loop surface should emit these standing KPIs:

KPI Meaning
loop_fire_total Trigger events received, by source.
loop_admit_total Fires admitted/refused, by reason.
loop_run_total Runs started/ended, by status.
loop_witness_rate Witnessed done / claimed done.
loop_schedule_drift_seconds Actual fire time minus intended fire time.
loop_stuck_age_seconds Oldest running run without heartbeat or evidence.
loop_notify_total Notifications sent/acked, by sink and reason.
loop_cache_reuse_ratio Cache/prefix reuse for loops sharing a gateway.
loop_budget_burn_without_evidence Tokens/time spent before first independent effect.

These are the loop-level counterparts to existing gateway and task-manager surfaces. They turn long-running automation from “a script probably ran” into a governed kernel object.

Verdict

fak is positioned well precisely because unattended loops widen the distance between a human and an action. The more a run moves into cloud VMs, other nodes, cron, phones, Slack, or WhatsApp, the more valuable a structural kernel boundary becomes.

The concrete gap is not another planner. It is a loop kernel surface: durable loop identity, trigger normalization, node/target admission, task/resource snapshots, witnessed completion, bounded notifications, and cache-aware scheduling. Build that and the existing scripts stop being one-off automation; they become user-space drivers for one governed agent OS.