SUZENT / 文档
SUZENT 使用手册

HTTP API and internals

Everything the desktop app does goes through the local HTTP API, on the same port as the app ( 25314 unless you set SUZENT_PORT ). This page collects the routes and architecture no

此页面目前提供英文内容,中文翻译尚未完成。

Everything the desktop app does goes through the local HTTP API, on the same port as the app (25314 unless you set SUZENT_PORT). This page collects the routes and architecture notes that used to be spread across the user guides.

Calling Suzent from Inside the Sandbox

SUZENT_BASE_URL is injected automatically so sandboxed code can reach the running host server. This lets agent-written scripts do everything the suzent CLI can do, without the CLI being installed.

VariableExample value
SUZENT_BASE_URLhttp://host.docker.internal:25314

CLI → API mapping:

CLI commandHTTP equivalent
suzent cron listGET $SUZENT_BASE_URL/cron/jobs
suzent cron add ...POST $SUZENT_BASE_URL/cron/jobs
suzent cron trigger {id}POST $SUZENT_BASE_URL/cron/jobs/{id}/trigger
suzent cron remove {id}DELETE $SUZENT_BASE_URL/cron/jobs/{id}
suzent nodes listGET $SUZENT_BASE_URL/nodes
suzent nodes describe {node_id_or_name}GET $SUZENT_BASE_URL/nodes/{node_id_or_name}
suzent nodes invoke {node_id_or_name} ...POST $SUZENT_BASE_URL/nodes/{node_id_or_name}/invoke
suzent agent chat "msg"POST $SUZENT_BASE_URL/chat (streaming)
List chatsGET $SUZENT_BASE_URL/chats
Memory searchGET $SUZENT_BASE_URL/memory/archival?query=...

Example — scheduling a cron job from sandboxed Python:

import os, requests

base = os.environ["SUZENT_BASE_URL"]

requests.post(f"{base}/cron/jobs", json={
    "name": "daily-report",
    "cron_expr": "0 9 * * *",
    "prompt": "Summarize today's activity",
    "delivery_mode": "announce",
})

Memory

Everything the Memory panel does is available over HTTP, on the same port as the app (25314 unless you set SUZENT_PORT), so you can script it or wire it into your own tools.

EndpointMethodWhat it does
/memory/coreGETRead the core blocks (persona, user, facts, context)
/memory/corePUTOverwrite one core block
/memory/fileGETRead MEMORY.md
/memory/dailyGETList the dates that have a daily log
/memory/daily/{date}GETRead one day's log
/memory/archivalGETSearch remembered facts
/memory/archival/{id}DELETEForget one fact (records a tombstone)
/memory/statsGETCounts and index size
/memory/project-contextsGETList per-project context files
/memory/project-contexts/{id}PUTUpdate a project's context
/memory/reindexPOSTRebuild the search index from Markdown
/memory/dream/statusGETConsolidation progress and pending work
/memory/consolidatePOSTRun consolidation now
/memory/lintPOSTRun the notebook audit now

Editing a core block through PUT /memory/core counts as you saying it, which outranks anything the agent worked out on its own — see MEMORY.md is half yours.

Automation

Scheduled tasks

MethodEndpointDescription
GET/cron/jobsList all jobs
POST/cron/jobsCreate a job
PUT/cron/jobs/{job_id}Update a job
DELETE/cron/jobs/{job_id}Delete a job
POST/cron/jobs/{job_id}/triggerTrigger immediate run
GET/cron/statusScheduler health and job counts
GET/cron/notificationsDrain pending announce notifications

Heartbeat

MethodEndpointDescription
GET/heartbeat/statusHeartbeat system status
POST/heartbeat/enableEnable heartbeat
POST/heartbeat/disableDisable heartbeat
POST/heartbeat/triggerTrigger immediate tick
GET/heartbeat/mdRead HEARTBEAT.md content
PUT/heartbeat/mdUpdate HEARTBEAT.md content
PUT/heartbeat/intervalSet interval ({"interval_minutes": N})

Scheduler architecture

There is one clock and one table. SchedulerBrain owns the timing for every scheduled task, including heartbeats; HeartbeatRunner owns execution inside a live conversation. Tasks live in the cron_jobs table, which kept its name from when cron was all there was.

┌──────────────────┐
│  SchedulerBrain   │  tick every 30s
│  ._tick()         │
└─────────┬────────┘
          │  1. project heartbeat-enabled chats onto task rows
          │  2. find rows with next_run_at <= now
          │     (skipping in-flight rows and stale missed runs)
          ▼
   ┌──────────────┐
   │ context_mode │
   └──┬────────┬──┘
      │        │
 isolated    bound ──── chat busy? ──► slide to next tick, no retry spent
      │        │
      ▼        ▼
┌───────────┐ ┌──────────────────────────────┐
│ cron-{id} │ │ HeartbeatRunner              │
│ chat, via │ │  .run_bound_turn()           │
│ ChatProc- │ │  background turn in the chat │
│ essor     │ │  suppress_ok → roll back a   │
└─────┬─────┘ │  turn with nothing to report │
      │       └───────────────┬──────────────┘
      └───────────┬───────────┘
                  ▼
        ┌──────────────────┐
        │ Record the run   │
        │ Re-arm or retire │
        │ Notify if asked  │
        └──────────────────┘

Heartbeat rows take one detour: instead of running straight away, the scheduler marks the chat pending and the runner gives an attached frontend 20 seconds to claim the turn and stream it where you can watch. Nobody claims it, the server runs it headlessly. That is why heartbeats produce no run history and no notification — they are not reportable task runs.

Heartbeat stays configured where the UI already writes it (chat.config plus the project's heartbeat.md); each tick reconciles that with the task row in both directions, so changing the interval re-arms the row, and a heartbeat the frontend ran itself pushes the row's next fire time out.

Notification Flow

Both systems deliver notifications through a shared mechanism:

  1. Cron jobs with delivery_mode: "announce" push results to an in-memory deque
  2. Heartbeat alerts route through the scheduler's notification deque via a callback
  3. Frontend polls GET /cron/notifications every 5 seconds
  4. Notifications appear in the status bar

Scheduler lifecycle

Server Lifecycle

Both systems start during server initialization (init_background_services()) and stop during shutdown:

  • SchedulerBrain — ticks every 30 seconds, checking for due jobs
  • HeartbeatRunner — sleeps for the configured interval (default 30 minutes)

Model Resolution

Both systems resolve which LLM model to use in this order:

  1. Job-level model_override (cron only)
  2. User preferences model (from settings)
  3. System default

Memory

Memory is disabled for both cron and heartbeat executions to avoid polluting the knowledge base with routine automated output.

GitHub Sync

All routes are served by the local Suzent HTTP API.

EndpointMethodDescription
/sync/statusGETProfile and repository status
/sync/quickstart/infoGETDefault paths and GitHub authentication state
/sync/quickstartPOSTCreate or connect the sync repository
/sync/profilesGET, POSTList or save profiles
/sync/planPOSTPreview file changes without retaining worktree mutations
/sync/diffPOSTLoad one selected file's textual diff
/sync/pullPOSTPull and apply portable files
/sync/pushPOSTBuild, commit, and push portable files
/sync/discard-outgoingPOSTRestore all outgoing changes, or selected paths
/sync/autoPOSTSave automation settings
/sync/auto/runPOSTRun one automatic sync cycle
/sync/auth/startPOSTStart GitHub Device Flow
/sync/auth/pollPOSTPoll Device Flow completion
/sync/auth/statusGETRead GitHub authentication state
/sync/auth/logoutPOSTClear the stored GitHub token

Sync repository layout

github-sync/
  suzent-sync/
    config/
      config.yaml
      default.yaml
      skills.json
    skills/
    memory/

The default repository path is ~/.suzent/github-sync. Sync profiles live in ~/.suzent/config/sync_profiles.json and are not portable because repository paths and automation settings are device-specific.

Social channels

Channel architecture

The system uses a driver-based architecture:

  1. ChannelManager: Central hub that manages platform drivers. Uses Dynamic Loading to load drivers specified in social.json.
  2. SocialChannel (Driver): Platform-specific implementation (e.g., TelegramChannel, FeishuChannel). Handles API polling/WebSockets and format conversion.
  3. UnifiedMessage: comprehensive internal message format.
  4. SocialBrain: Core logic that bridges the social message queue to the AI Agent. Handles:
    • Security checks.
    • Attachment processing (downloading to sandbox).
    • Agent instantiation (get_or_create_agent).
    • Response streaming.

Background service

Platform integration

Suzent installs a current-user service and does not require administrator access:

PlatformIntegrationRecovery policy
WindowsPer-user HKCU\\...\\Run entryUp to three retries with five-second backoff
macOSLaunchAgentRestart after an unexpected exit
Linuxsystemd --userRestart=on-failure

The service binds only to 127.0.0.1. A private, per-process control token is required for the graceful stop endpoint; the token is never returned by health or status APIs. Runtime state is validated against both PID and process creation time so a recycled PID cannot be mistaken for Suzent.

Resource behavior

Idle memory depends on enabled providers, memory indexing, channels, and native libraries. Suzent bounds the data structures it owns rather than claiming a fixed footprint on every machine:

  • pending UI notifications are durable and capped at 1,000 records;
  • LanceDB and memory indexing initialize on the first agent turn or Memory API use;
  • completed host processes expire after 10 minutes and retained metadata is capped;
  • host command output is capped at 16 MiB per background process;
  • streaming queues and pending approval records have fixed limits;
  • the service samples RSS once per minute and gracefully recycles after five consecutive samples above 1,024 MiB.

Set SUZENT_SERVICE_MAX_RSS_MB to change the watchdog threshold. Values below 256 MiB are clamped because the full agent runtime may legitimately need more. SUZENT_SERVICE_RSS_INTERVAL changes the sampling interval, with a five-second minimum. Platform supervision starts a fresh process after watchdog recycling.

在 GitHub 上编辑此页 ↗

本页内容