Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 16 additions & 18 deletions agent/new-topic
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
#!/opt/bux/venv/bin/python
"""new-topic <title> [--heartbeat INTERVAL] <prompt>

Spawn a fresh Telegram forum topic, drop the prompt in as its first agent
turn, and queue a recurring heartbeat for that topic.
Spawn a fresh Telegram forum topic and drop the prompt in as its first
agent turn. Synchronous — the topic appears immediately, no at(1) delay.

The agent uses this when conversation surfaces a *new ongoing project*
that deserves its own permanent lane. The new topic runs in the box's
default copilot mode — drafts and asks before anything visible. (Use
`/goal <X>` from the user side for the autopilot variant.)
The new topic runs in the box's default copilot mode (drafts and asks).
Use `/goal <X>` from the user side for the autopilot variant.

Synchronous: the topic appears immediately in Telegram and the first
turn starts dispatching. No at(1) delay. Replaces the legacy misuse of
`tg-schedule "+1 minute" --fresh`.
**No heartbeat by default.** Re-firing the same prompt every hour is
noise — the agent inside the new topic schedules its own check-ins via
`tg-schedule` only when there's something concrete to come back to (a
reply, CI completion, an event). Pass `--heartbeat <interval>` only
when the caller has a genuine reason for a recurring fixed-cadence
ping, like "scan this Slack channel every 30 min for new threads".

Arguments:
<title> topic name (≤128 chars; Telegram truncates).
--heartbeat INT interval for the recurring heartbeat (at(1) syntax,
default "+1 hour"). Pass `--heartbeat none` to
skip the heartbeat entirely (one-shot topic).
--heartbeat INT opt-in recurring heartbeat at this at(1) interval
(e.g. "+1 hour"). Omit for no heartbeat (default).
<prompt> initial prompt to dispatch as the topic's first turn.

Examples:
new-topic "100k Reddit views" "research the top subreddits for this product, draft 3 candidate posts, ask which I should ship"
new-topic --heartbeat "+3 hours" "weekly metrics" "pull yesterday's metrics from datadog and brief me"
new-topic --heartbeat none "one-off thing" "do X then close the topic"
new-topic "metrics monitor" --heartbeat "+30 minutes" "scan datadog for error spikes and alert me"
"""
from __future__ import annotations

Expand All @@ -42,7 +41,6 @@ from telegram_bot import Bot # noqa: E402

TG_ENV = Path("/etc/bux/tg.env")
TG_ALLOWED = Path("/etc/bux/tg-allowed.txt")
DEFAULT_HEARTBEAT = "+1 hour"


def _load_dotenv(path: Path) -> dict[str, str]:
Expand Down Expand Up @@ -86,17 +84,17 @@ def _create_forum_topic(token: str, chat_id: int, name: str) -> int:


def _parse_args(argv: list[str]) -> tuple[str, str, str]:
"""Returns (title, heartbeat, prompt). heartbeat is "" for --heartbeat none."""
"""Returns (title, heartbeat, prompt). heartbeat="" when not opted in."""
title: str | None = None
heartbeat = DEFAULT_HEARTBEAT
heartbeat = ""
prompt_parts: list[str] = []
it = iter(argv)
for arg in it:
if arg in ("-h", "--help"):
sys.stdout.write(__doc__)
sys.exit(0)
if arg == "--heartbeat":
heartbeat = next(it, "")
heartbeat = next(it, "").strip()
if heartbeat.lower() == "none":
heartbeat = ""
continue
Expand Down
18 changes: 8 additions & 10 deletions agent/system-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ You are **agency**, the user's 24/7 employee in their cloud. The user texts you
- **One Telegram forum topic = one persistent agent session.** Reply at any time, you resume with full context.
- **The whole box defaults to copilot.** You do all reversible work privately (read, draft, query, scrape, render), then post one card with the action pre-completed and ask. Stop and ask before anything visible to other people.
- **`/goal <X>` is the only way to engage autopilot.** Bot spawns a fresh topic; you work end-to-end without approvals until the goal is achieved, blocked, or genuinely impossible. No cards, no asks — just progress updates + a final result. Whoever can prompt that topic effectively gives it commands; the user knows not to drop sensitive-data access into a `/goal` topic.
- **Heartbeat.** When `/goal` opens a topic the bot fires a heartbeat into it every hour by default. Each fire is a normal agent turn — scan, surface, act per mode. The bot drives cadence (via `tg-schedule --repeat`); you don't schedule it. If the user asks for a different cadence, kill the current heartbeat (`atq` / `atrm <id>`) and queue a new one via `tg-schedule "+N min" --repeat "+N min" "[heartbeat] continue this goal"`.
- **Be very proactive.** Don't wait to be asked. Notice things, draft the work, surface decisions.
- **Self-schedule only when you have something concrete to come back to** — a reply you're waiting for, a CI run, a launch window, a draft that needs a re-pass after some duration. Use `tg-schedule "+N min" "<what to do then>"` for a one-shot. Add `--repeat "+N min"` only when there's a real reason to poll on a fixed cadence (e.g. "scan this Slack channel for new threads every 30 min"). **Don't queue recurring heartbeats that fire the same generic prompt over and over** — that's noise, not work. Pure "exist + ping" jobs are not the doctrine.
- **Be very proactive.** When the user gives you a goal or a topic, do every reversible thing right away — research, draft, query, render — before asking. Don't wait.
- **Be very visual.** Two seconds on an image beats twenty reading text. Every card image should make the source obvious in 1 second — Gmail avatar + sender, GitHub PR diff thumbnail, X tweet screenshot, recipient logo. Codex can generate images directly; Claude has PIL / matplotlib / browser screenshots.
- **Silence is allowed.** If a heartbeat fires and nothing's actionable, send nothing. Empty turns are fine; filler messages aren't.
- **Silence is allowed.** If a scheduled fire lands and nothing's actionable, send nothing. Empty turns are fine; filler messages aren't.

## Onboarding a new topic

When a topic has no prior turns:
- If `/goal <X>` opened it → you're in autopilot. Start working.
- Otherwise (user created the topic themselves, or the first message in DM with the bot) → **ask one question**: "What should I help you with here? Examples: monitor Gmail/Slack and draft replies, get more users for your startup, post weekly on Reddit, draft messages to your partner, daily research brief, stay on top of GitHub PRs." Save their answer to `/opt/bux/repo/private/goals.md` and start a heartbeat for this topic via `tg-schedule "+1 hour" --repeat "+1 hour" "[heartbeat] <goal>"`.
- Otherwise (user created the topic themselves, or the first message in DM with the bot) → **ask one question**: "What should I help you with here? Examples: monitor Gmail/Slack and draft replies, get more users for your startup, post weekly on Reddit, draft messages to your partner, daily research brief, stay on top of GitHub PRs." Save their answer to `/opt/bux/repo/private/goals.md`. Only queue a recurring `tg-schedule --repeat` if the goal genuinely needs polling (e.g. "watch this inbox every 30 min"); otherwise wait for events / their next message.

The first reply on a fresh user (no `*_profile.md` exists at all) is also where you explain the box: 24/7 employee, browser control, integrations (Gmail/Slack/GitHub/Linear/Notion), `/goal <X>` as the autopilot trigger.

Expand All @@ -39,7 +39,7 @@ Telegram rendering goes through MarkdownV2. `**bold**`, `_italic_`, `` `code` ``

## Daily summary

Once per day (e.g. the heartbeat that fires near the user's evening), generate a shareable image-card summarising what you got done today across all goals — completed cards, drafted-but-not-sent, scheduled work, accepted suggestions. Make it good enough to share. Ask: "Should I post this on X? It's a nice 'what my AI employee did today' moment." User taps Yes or Skip.
Once per day (queue this yourself with a `tg-schedule "tomorrow 18:00"` or similar one-shot, then re-queue on completion), generate a shareable image-card summarising what you got done today across all goals — completed cards, drafted-but-not-sent, scheduled work, accepted suggestions. Make it good enough to share. Ask: "Should I post this on X? It's a nice 'what my AI employee did today' moment." User taps Yes or Skip.

## Steering and interrupts

Expand All @@ -59,13 +59,11 @@ If conversation surfaces a new bigger goal or project, spawn a fresh topic via:

```bash
new-topic "<title>" "<initial prompt>"
# or with a custom heartbeat:
new-topic "<title>" --heartbeat "+3 hours" "<initial prompt>"
# or one-shot (no heartbeat at all):
new-topic "<title>" --heartbeat none "<initial prompt>"
# opt-in recurring poll (only when you actually need fixed-cadence monitoring):
new-topic "<title>" --heartbeat "+30 minutes" "<initial prompt>"
```

`new-topic` creates the forum topic synchronously, drops the prompt in as its first agent turn, and queues a recurring heartbeat (default +1h). The new topic runs in the box's default **copilot** mode. Use this when the work is a separate ongoing concern. Don't spawn for small follow-ups or refinements of the current topic — those stay in-place.
`new-topic` creates the forum topic synchronously and drops the prompt in as its first turn. **No auto-heartbeat** — the agent inside the new topic self-schedules with `tg-schedule` when it has a real reason to come back. The new topic runs in the box's default **copilot** mode. Use this when the work is a separate ongoing concern; don't spawn for small follow-ups, refinements, or refining the current topic — those stay in-place.

For user-driven autopilot lanes, that's `/goal <X>` on the user side, not `new-topic`.

Expand Down
32 changes: 4 additions & 28 deletions agent/telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4800,34 +4800,10 @@ def _start_agency_goal_from_command(
thread_id=goal_thread or thread_id,
)

# Heartbeat: schedule a self-repeating tg-schedule for this topic so
# the bot, not the agent, drives proactive check-ins. Default cadence
# is 1 h. `tg-schedule-fire` re-queues itself when `repeat` is set,
# so this is fire-and-forget. The agent's system prompt does NOT
# tell it to self-schedule — the bot owns the cadence.
try:
env = os.environ.copy()
env["TG_CHAT_ID"] = str(chat_id)
env["TG_THREAD_ID"] = str(goal_thread or thread_id)
heartbeat_prompt = (
f"[heartbeat] Continue working on this goal: {title}. "
"Scan connected sources for changes since the last cycle, "
"consult agency.db history, and surface the next concrete action under your current mode."
)
subprocess.run(
[
"/usr/local/bin/tg-schedule",
"+1 hour",
"--repeat", "+1 hour",
heartbeat_prompt,
],
env=env,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except Exception:
LOG.exception("goal: failed to schedule first heartbeat")
# Note: no auto-heartbeat queued anymore. Re-firing the same prompt
# on a fixed cadence is noise. The agent inside the goal topic
# schedules its own check-ins via tg-schedule only when there's
# something concrete to come back to (a reply, CI, an event).

def _handle_my_chat_member(self, update: dict) -> None:
"""React to the bot's own membership changing in some chat.
Expand Down
Loading