Bootstrap

This isn't a new idea.

OpenClaw exists. Open-source, runs on your machine, talks to you over Telegram. Thousands of GitHub stars. You could clone it and be running in an afternoon.

But OpenClaw isn't Friday. And the difference isn't the feature list.

Every previous attempt at something like this hits the same wall: frontier models are capable but expensive on a per-token API key, and cheaper models aren't capable enough to trust with real tasks. That's not an assistant. That's autocomplete with overhead.

Friday runs on ACP – the same protocol Claude Code runs on – and that's what made this one actually get finished.

Why ACP

ACP is the Agent Computer Protocol, exposed through @anthropic-ai/claude-agent-sdk. It's the same protocol Claude Code runs on.

That last part matters more than it sounds. Using ACP means we're billed against a Claude subscription – not an API key with per-token metering. No separate key to manage. No usage dashboard to watch. No bill at the end of the month that scales with how much we used our own assistant.

There's a second constraint it satisfies: no LLM is used outside of Claude Code. Friday doesn't call OpenAI, Gemini, or any other model. Every tool call, every scheduled task, every Telegram message that gets a response – all of it routes through the same ACP session. Billing stays predictable. The model stays consistent.

ACP also gives us things the raw HTTP API doesn't. There's a canUseTool callback – a function we provide that decides whether a given tool call is allowed to proceed. There's a session model built for persistence, not one-shot requests. There's streaming output that maps cleanly to a Telegram message that edits itself as content arrives.

By the end of the first feature, the Telegram bridge, the HTTP injection endpoint, and the scheduler heartbeat all funnel into a single ACP session. The session is the hub. Everything else is plumbing around it.

Why Telegram

Telegram is already on your phone. That's most of the argument.

When Claude asks to run a shell command, a message arrives with approve and deny buttons. You tap approve. The task continues. You're on the bus, not at the machine.

The other thing Telegram gives us is a channel for the assistant to reach out – not for us to reach it. A scheduled task fires at 3am, runs a health check, sends a summary. A background task finishes and says so. That outbound direction doesn't exist in a CLI model.

There's more setup up front. But you do it once, and then it's just there – persistent, restartable, running whether you're at the machine or not.

The Session Model

Friday runs one persistent ACP session. It starts on boot, and everything – Telegram messages, scheduled tasks, HTTP prompts – routes through it.

That matters because Claude remembers what happened. You can interrupt a task, come back an hour later, and the context is still there. A fresh session per request is stateless. Easy to reason about. Also useless for an assistant.

The SDK supports this with two things: persistSession: true in the query options, and options.resume to pass back a previous session ID. The session ID itself comes back in a system/init message – watch for it in the message stream:

for await (const message of session) {
    if (message.type === 'system' && message.subtype === 'init') {
        await db.set('session_id', message.session_id)
    }
    // handle other message types...
}

On the next startup, we read the stored ID and pass it back:

const sessionId = await db.get('session_id')

const session = client.query(prompt, {
    persistSession: true,
    ...(sessionId ? { resume: sessionId } : {}),
})

The cost of this model is thinking about what happens when the session grows too large. The SDK handles compaction automatically – it fires a compact_boundary event and compresses the context – but you have to trust it. More on that in a bit.

Three Paths, One Session

By the end of the first feature, three separate paths all lead to the same ACP session.

The Telegram bridge listens for incoming messages via long-polling. When a message arrives, it injects a prompt into the session and streams the response back – editing the same Telegram message in place as content arrives, so you see it building rather than waiting for a complete reply.

The scheduler heartbeat runs on a 30-second interval, checking whether any configured tasks are due. When one fires, it auto-injects its prompt into the live session. A 5-minute health check, a daily summary, a reminder to review something – all of it routes to ACP without any manual action.

The HTTP injection endpoint lets any process on the machine POST a prompt to localhost:3100/prompt and have the session pick it up. This is how Claude Code skills trigger Friday actions – a skill running in Claude Code can reach out to Friday mid-session, and Friday responds through the same persistent session.

Diagram

When Claude asks to use a tool that requires permission, the canUseTool callback fires. Friday sends a Telegram message with approve and deny buttons. Tapping approve unblocks the callback and the tool call proceeds. The session never drops.

This three-path architecture is the template for everything that follows. Future features add more surface area – MCP tool registration, background task agents, a writing assistant – but each one is another path into the same session, not a new model or a new billing relationship.

Not Yet grammY

The plan going in was to use grammY.

It's a well-maintained Telegram bot framework with first-class TypeScript support, plugins for message splitting and parse-mode handling, and a runner with graceful shutdown built in. It was the obvious choice. Good docs. Clear upgrade path.

Then the actual scope of the first feature became clear: a single-user bot. One allowed chat ID. No command routing. The approve and deny buttons are two reply markup options. That's it. The graceful shutdown grammY's runner provides is appealing – but the raw fetch-based polling loop that replaces it is twenty lines and has no surprising behaviour.

So the first feature skips it. Not because grammY is wrong, but because the first feature is too narrow to justify the surface area. It'll come back once the bot has enough complexity to earn it.

The Compaction Dead-End

A persistent session has a context window, and eventually it fills up. When it does, the SDK compacts automatically – it fires a compact_boundary event in the message stream and compresses the conversation history down to something that fits. The session continues. You don't have to do anything.

The obvious follow-up question is whether you can trigger that yourself. Proactively, before the context gets close to the limit. Between tasks, when you know the timing is safe, rather than mid-conversation when it isn't.

The SDK types suggest yes: compact boundary messages have a trigger field typed as 'manual' | 'auto'. The word "manual" implies an API surface to call.

There isn't one. Not in this version of the SDK.

supportedCommands() doesn't include /compact. Injecting /compact as a prompt doesn't work – it gets treated as a message, not a command. The type exists, but the API to use it doesn't seem to be there yet. Ask me how I know.

So the approach for now is to trust autocompaction and design the session to be resumable from cold if something goes wrong – storing enough in SQLite that a fresh session can be brought up to speed quickly, not relying on context alone.

Conclusion

The daemon runs as a systemd user service. It restarts automatically if it crashes. The session survives a reboot.

The practical difference from a CLI session is hard to quantify but immediately felt. There's a continuity to things. Claude knows what it was working on this morning. You can interrupt a long task – approve a tool use, add a constraint, change direction – and it picks up from where it was. The cost is the same subscription you were already paying.

That continuity is the real product. Not the Telegram integration, not the scheduler, not the HTTP endpoint. Those are delivery mechanisms. What they deliver is a persistent context that costs nothing extra to run and that you can reach from anywhere.

The three-path architecture from the first feature turns out to be the right abstraction. Every subsequent feature is another path into the session: MCP tools that let Claude reach into Friday's own data stores, background task agents that run in parallel sub-sessions, a writing assistant with its own context window. Each one adds surface area without changing the core – ACP is still the hub, the session is still the thing.