EN 中文

# Cron & Wakeup

# 定时任务 & 唤醒

Scheduled prompts, model self-wakeups, external triggers — v5 柱 H (shipped v0.6.1). Zero resident daemon.

定时 prompt、模型自我唤醒、外部触发——v5 柱 H（v0.6.1 已 ship）。零常驻 daemon。

seek doesn't run a background process. Instead, it teaches your OS scheduler (launchd / systemd / Task Scheduler) to invoke `seek cron tick` once a minute. seek's tick reads `~/.seek/cron/jobs.jsonl` + `triggers/`, fires whatever's due, exits.

seek 自身不跑后台进程。改成教你的 OS 调度器（launchd / systemd / Task Scheduler）每分钟调用一次 `seek cron tick`。tick 读 `~/.seek/cron/jobs.jsonl` + `triggers/`，触发所有到期任务，退出。

## 1. Register a job

## 1. 注册任务

    $ seek cron create --name ci-watch --at @daily \
        --cwd ~/code/myproj \
        'check main branch CI status; summarise failures'

    $ seek cron list             # see what's registered
    $ seek cron run ci-watch     # fire NOW, bypass schedule

Saved to `~/.seek/cron/jobs.jsonl`. But nothing fires yet — the OS scheduler hasn't been wired up. Continue below.

保存到 `~/.seek/cron/jobs.jsonl`。但还不会自动跑——OS 调度器还没启动 tick，继续下面。

## 2. Wire up the OS scheduler

## 2. 接 OS 调度器

macOS · launchd
Linux · systemd
Windows · Task Scheduler

    $ mkdir -p ~/Library/LaunchAgents
    $ cat > ~/Library/LaunchAgents/com.seek.cron.plist <<'EOF'
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>Label</key><string>com.seek.cron</string>
      <key>ProgramArguments</key>
        <array>
          <string>/usr/local/bin/seek</string>
          <string>cron</string>
          <string>tick</string>
        </array>
      <key>StartInterval</key><integer>60</integer>
      <key>RunAtLoad</key><true/>
    </dict>
    </plist>
    EOF
    $ launchctl load ~/Library/LaunchAgents/com.seek.cron.plist

Replace `/usr/local/bin/seek` with your actual `which seek`. Uninstall: `launchctl unload` + `rm`.

把 `/usr/local/bin/seek` 改成你的 `which seek` 路径。卸载：`launchctl unload` + `rm`。

    $ mkdir -p ~/.config/systemd/user

    $ cat > ~/.config/systemd/user/seek-cron.service <<'EOF'
    [Unit]
    Description=seek cron tick

    [Service]
    Type=oneshot
    ExecStart=%h/.local/bin/seek cron tick
    EnvironmentFile=-%h/.seek/cron/env
    EOF

    $ cat > ~/.config/systemd/user/seek-cron.timer <<'EOF'
    [Unit]
    Description=Fire seek cron tick every minute

    [Timer]
    OnBootSec=1min
    OnUnitActiveSec=1min

    [Install]
    WantedBy=timers.target
    EOF

    $ systemctl --user daemon-reload
    $ systemctl --user enable --now seek-cron.timer

The `EnvironmentFile=-` prefix means "no error if missing" — pairs with the env overlay in §3 below. Check: `systemctl --user list-timers`, `journalctl --user -u seek-cron.service`.

`EnvironmentFile=-` 前缀的 `-` 意味"文件不存在也不报错"——配合下面 §3 的 env overlay 一鱼两吃。检查：`systemctl --user list-timers`、`journalctl --user -u seek-cron.service`。

    PS> schtasks /create /tn "seek cron tick" /tr "seek cron tick" /sc minute /mo 1 /ru "$env:USERNAME"

Or GUI: Task Scheduler → Create Basic Task; trigger Daily + Repeat every 1 minute; action Start a program (`seek`) with args `cron tick`. Uninstall: `schtasks /delete /tn "seek cron tick" /f`.

或 GUI：任务计划程序 → 创建基本任务；触发器选每天 + 重复任务每 1 分钟；操作启动程序 `seek` 参数 `cron tick`。卸载：`schtasks /delete /tn "seek cron tick" /f`。

## 3. Inject the API key — `~/.seek/cron/env`

## 3. 注入 API key——`~/.seek/cron/env`

Core problem: the OS scheduler hands your `seek cron tick` process a near-empty environment. `launchd` typically gives `PATH=/usr/bin:/bin` and nothing else; `systemd` user units inherit only what `EnvironmentFile=` names; Windows Task Scheduler is similar. Your `.zshrc`'s `DEEPSEEK_API_KEY` will not cross that boundary automatically.

核心问题：OS 调度器拉起 `seek cron tick` 时 env 极简。`launchd` 通常只给 `PATH=/usr/bin:/bin`；`systemd` 用户单元只继承 `EnvironmentFile=` 列出的；Windows Task Scheduler 也类似。你 `.zshrc` 里的 `DEEPSEEK_API_KEY` 完全不会自动跨过这条边界。

Fix: write a dotenv-style file at `~/.seek/cron/env` — seek auto-overlays it onto every spawned cron child:

解法：写一个 dotenv-style 文件 `~/.seek/cron/env`——seek 自动叠加到每个 cron 子进程：

    # ~/.seek/cron/env (macOS / Linux / WSL)
    # %USERPROFILE%\.seek\cron\env (Windows)

    DEEPSEEK_API_KEY=sk-...
    PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin
    # Linux desktop notifications additionally need:
    # DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

Format: one `KEY=VALUE` per line; `#` comments; balanced quotes stripped; no shell expansion (`$HOME` is three literal characters). Duplicate keys: last wins. Parse errors (missing `=`, empty key) fail spawn loudly — far safer than silently running without the API key you thought you set.

格式：一行一个 `KEY=VALUE`；`#` 注释；平衡引号被剥离；不做 shell 展开（`$HOME` 是字面三个字符）。重复 KEY 后写赢。解析错误（缺 `=`、空 KEY）→ 明确报错，spawn 失败——比"静默掉 API key"安全多了。

💡 systemd users: pointing `EnvironmentFile=%h/.seek/cron/env` at this same file makes it drive both systemd's env AND seek's overlay — one file, two layers. 💡 systemd 用户：`EnvironmentFile=%h/.seek/cron/env` 让同一个文件同时驱动 systemd 的 env 和 seek 的覆盖——一文件双用。

## 4. OS notifications

## 4. OS 通知

Platform | 平台 | Implementation | 实现 | Status | 状态
--- | --- | --- | --- | --- | ---
macOS | `osascript -e 'display notification ...'` | ✅ Notification Center banner
Linux | `notify-send` (libnotify) | ✅ Desktop (auto no-op when `$DISPLAY` + `$WAYLAND_DISPLAY` empty) | ✅ 桌面（`$DISPLAY`+`$WAYLAND_DISPLAY` 都空时自动 no-op）
Windows | (none) | （无） | ⚠️ v0.6.1 no-op — BurntToast adapter planned for v0.6.x dot | ⚠️ v0.6.1 暂为 no-op——BurntToast 适配规划在 v0.6.x dot

⚠️ Windows fallback: until a toast adapter ships, rely on `seek cron list` for `last_status` and `tail -F %USERPROFILE%\.seek\cron\runs\*.jsonl` for run output. ⚠️ Windows 临时方案：toast 适配落地前，依赖 `seek cron list` 看 `last_status` + `tail -F %USERPROFILE%\.seek\cron\runs\*.jsonl` 查最近运行。

## 5. External triggers (file bridge)

## 5. 外部触发（文件桥）

CI, IDE plugins, shell scripts can write JSON to `~/.seek/cron/triggers/` — the next tick consumes + deletes:

CI、IDE 插件、shell 脚本可写 JSON 到 `~/.seek/cron/triggers/`——下一次 tick 消费 + 删除：

    $ cat > ~/.seek/cron/triggers/ci-$(date +%s).json <<EOF
    {
      "trigger_id": "ci-build-1234",
      "prompt": "CI build 1234 finished on branch foo-feature; summarise test failures",
      "cwd": "/Users/me/code/myproj",
      "ttl_seconds": 3600
    }
    EOF

Rules: file mtime must be older than "now − 1s" (prevents tick reading partial writes); parse failure → file renamed to `triggers/.malformed/<id>.json` + WARN, doesn't block; `ttl_seconds` expired → silently dropped; consumed → deleted (the directory is an inbox, not history). Run history lives in `~/.seek/cron/runs/<id>.jsonl` with two-axis GC (keep 100 recent + 30d max-age).

规则：文件 mtime 必须早于"现在 − 1s"（防 tick 读半截）；解析失败 → 文件 rename 到 `triggers/.malformed/<id>.json` + WARN，不阻塞；`ttl_seconds` 过期 → silently 丢弃；跑完即删（目录是 inbox，不是历史）。历史在 `~/.seek/cron/runs/<id>.jsonl`，双轴 GC（保留 100 个最近 + 30 天最老）。

## 6. `schedule_wakeup` LLM tool

## 6. `schedule_wakeup` LLM 工具

The model can schedule a future check-in on itself — e.g., "30 minutes from now, look at CI again." Internally this is just a one-shot cron job (`max_runs=1`) registered through the same `jobs.jsonl` pipeline.

模型可以给自己安排未来回访——比如"30 分钟后再来检查 CI"。本质是通过同一 `jobs.jsonl` 管道注册一次性 cron 任务（`max_runs=1`）。

    # Model calls (not user):
    schedule_wakeup({
      "seconds": 1800,
      "prompt": "Check CI for branch foo-feature; report any new failures"
    })
    # → registered as cron job; fires once; auto-deletes

## 7. Troubleshooting

## 7. 故障排查

Symptom | 症状 | Likely cause / check | 大概率原因 / 检查
--- | --- | --- | ---
Registered a job, never fires | 注册了 job，从来不跑 | OS scheduler not running. macOS: `launchctl list | grep seek` · Linux: `systemctl --user list-timers` · Windows: `schtasks /query /tn "seek cron tick"` | OS 调度器没启。macOS: `launchctl list | grep seek` · Linux: `systemctl --user list-timers` · Win: `schtasks /query /tn "seek cron tick"`
Fires but every run auth-fails | 跑了但每次 auth fail | Subprocess env missing API key. Check `~/.seek/cron/env` exists; `seek cron run <name>` to reproduce | 子进程 env 没 API key。检查 `~/.seek/cron/env` 是否存在；`seek cron run <name>` 重现
Linux: "notify-send: command not found" | Linux 报 "notify-send: command not found" | `libnotify` not installed. `apt install libnotify-bin` / `dnf install libnotify` | libnotify 没装。`apt install libnotify-bin` / `dnf install libnotify`
Windows: notifications enabled but no popup | Windows 启用通知但没弹窗 | Known v0.6.1 limitation (see §4). Fallback to `seek cron list`. | v0.6.1 已知限制（见 §4）。暂时 fallback 到 `seek cron list`
Same job triggered twice | 同名 job 重复触发 | Prior run still active — per-job lock skips new trigger (design behavior; check `runs/<id>.jsonl` for `WARN: prior run still active`) | 上次 run 还没结束，per-job 锁跳过新触发（设计行为，看 `runs/<id>.jsonl` 的 `WARN: prior run still active`）
Env file parse error fails spawn | env 文件解析错就 spawn fail | By design — far safer than silent. Fix the syntax; re-run. | 设计如此——比静默更安全。修文件语法；再跑

📖 Full design + risk discussion: feature-routines.md · markdown guide for direct command paste: guide-cron.md · book chapter on routines architecture: chapter 22. 📖 完整设计 + 风险讨论：feature-routines.md · 直接粘贴命令的 markdown 指南：guide-cron.md · routines 架构 book 章：chapter 22。