fortytwo
Architecture
Packages
Design
forty-two.it
GitHub
Architecture
Packages
Design
forty-two.it
GitHub
  • Packages

    • @justfortytwo/installer
    • @justfortytwo/persona
    • @justfortytwo/memory
    • @justfortytwo/runner
    • @justfortytwo/scheduler
    • @justfortytwo/telegram
    • @justfortytwo/gate
    • @justfortytwo/salience
    • @justfortytwo/marketplace

@justfortytwo/scheduler

Durable job scheduler daemon for fortytwo. Drains a jobs queue stored in @justfortytwo/memory and fires LLM turns only behind a cheap "is there work?" gate.

The design rationale — durable jobs, token gating, crash safety, event hooks — is in Scheduler & Durable Jobs. This page documents the package as built.

  • Version: 1.0.0-3 · Runtime: standalone daemon, bin is fortytwo-scheduler · Node: >= 20

Why this exists

Proactive behavior (briefings, sweeps, reminders) needs durable time: work that survives restarts, is de-duplicated, and never spends an LLM turn when there is nothing to do. The scheduler owns when work happens, never how the agent reasons about it.

Components

ComponentFileRole
Handler registrysrc/registry.tsmaps job kind → Handler
tick()src/tick.tspure, fully injectable: claim due jobs → gate check → enqueue run → notify → reschedule/complete; backoff on error
Daemonsrc/daemon.tscroner + p-queue (concurrency 1) wiring around tick(), ~30 s poll
Recurring seedssrc/seed.ts, src/handlers/index.tsidempotent seeding of RECURRING_DEFS on every boot; only inserts/updates rows when a definition changes
Heartbeatsrc/heartbeat.tsliveness file for fortytwo doctor
Oneirossrc/oneiros.tspersona maintenance job (see below)
Notifiersrc/notifier.tsoptional Telegram push

Library usage

import { createRegistry, tick } from '@justfortytwo/scheduler';

const registry = createRegistry([myHandler]);

await tick({
  claimDue, complete, fail, registry,
  recurrenceNext, enqueueRun, notify,
}, new Date().toISOString());

Handler shape: { kind, run(ctx), hasWork?(ctx) }. A hasWork gate is the cheap check that runs before an LLM turn is spent.

Job kinds (as built)

kindschedulegatebehavior
daily_briefing57 7 * * * UTCnoneruns the daily-briefing skill and sends the rundown
sweep3 13,18 * * *pending approvals > 0open-thread sweep, one-line status
learn_review17 9 * * 0recent memories (7 d) > 0propose-only pattern promotion from the Journal
notify_pendingone-offnonenotify about pending items
replyone-offnonesend a reply via the runner
reminderone-offnonesend a reminder via the runner
reembed_memorymaintenancenonere-embeds one memory by id in-place (no LLM, no notification)
persona.oneiros17 3 * * * UTCmemory-head checkpersona maintenance (below)

Oneiros maintenance

persona.oneiros runs daily at 17 3 * * * UTC with null payload and the existing durable retry/claim mechanism. The installer seeds it after initializing persona's checkpoint; daemon boot reconciles the same job when the checkpoint exists. The cheap gate checks the memory head.

Persona validates and applies additions; the scheduler runs the private prompt through @justfortytwo/runner in an isolated temporary cwd:

--safe-mode --tools '' --strict-mcp-config --mcp-config '{"mcpServers":{}}'
--disable-slash-commands --no-session-persistence --setting-sources ''
--system-prompt <workerPrompt>

No tools, MCP, skills or session persistence are enabled; maintenance emits no notification into memory. Authentication/provider configuration is preserved. Requires a Claude CLI with --safe-mode — unsupported flags fail closed.

Running it

The daemon is a long-running process — run it under a restart loop (tmux for development, systemd for production) or under fortytwo start, which supervises it together with the Telegram bridge.

VariableRequiredDescription
DB_PATHyesabsolute path to fortytwo.db
EMBED_MODELrecommendedOllama embedding model (falls back to FakeEmbedder)
OLLAMA_BASE_URLnodefault http://localhost:11434
CLAUDE_BINnoclaude CLI path the runner spawns (default: claude on PATH)
FORTYTWO_TURN_TIMEOUTnoper-turn timeout in seconds (default 300)
TELEGRAM_BOT_TOKENoptionalenables Telegram push notifications
ALLOWED_CHAT_IDSoptionalcomma-separated chat IDs to push to

Liveness

On boot and on every 30-second poll tick — even while a previous tick is still in flight — the daemon writes $(dirname $DB_PATH)/scheduler.heartbeat containing { "pid", "ts" }. The write is outside the poll's overlap guard so a long-running turn never starves it. fortytwo doctor warns (non-fatal) if the file is missing or older than 90 seconds.

Dependencies

  • @justfortytwo/memory (pinned 1.0.0-2) — the jobs table and JobStore.
  • @justfortytwo/runner — LLM turns for handlers.
  • @justfortytwo/persona (pinned 1.0.0-2) — Oneiros hasWork/refine/ workerPrompt.
  • @justfortytwo/telegram — optional peer; enables the Telegram notifier.

Testing

npm run build
npm test        # vitest run

Scope

In scope: claiming, gating, recurrence math, retries/backoff, seeding, heartbeat, notifications.

Out of scope: what the agent does during a turn (runner + Claude Code), where jobs live (memory's jobs table), channel identity (telegram).

Edit this page
Last Updated: 10/20/26, 9:42 PM
Prev
@justfortytwo/runner
Next
@justfortytwo/telegram