npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zhushanwen/pi-scheduler

v0.3.2

Published

定时任务调度扩展:按 duration(`5m` / `2h` / `1d`)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once)、强制触发(force)与过期策略(expires)。任务随 owner session 持久化,resume 后继续触发。

Readme

scheduler

定时任务调度扩展:按 duration(5m / 2h / 1d)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once)、强制触发(force)与过期策略(expires)。任务随 owner session 持久化,resume 后继续触发。

产品定位

pi-scheduler 是 session 存活期间的 AI 提醒器——在 pi 进程运行、session 打开时,按计划向当前 session 注入消息。

非系统级 cron

  • pi 进程不开 = 不触发(与常驻后台的系统 cron daemon 不同,本扩展不是后台守护进程)
  • 电脑睡眠 / 关机 = 不触发
  • 不依赖系统 crontab,不注册任何开机自启

任务归属创建它的 session:任务物理存储在创建它的 session 的 JSONL 文件内,只在 owner session 打开(继续对话 / resume)时才触发

这是设计决策,不是 bug(D9):本扩展从 cwd 级定时器改为 session 级 AI 提醒器——任务归属创建它的 session,只在 owner session 存活时触发。如果你每天开新 session,昨天建的"明早检查 CI"任务今天不会响,除非你 resume 昨天创建该任务的那个 session。这不是"任务丢了":任务随 session 持久化,session 不打开就不调度。用户若发现「昨天建的任务今天没响」,这是预期行为,请 resume 创建该任务的 session。

简介与安装

pi-scheduler 是 xyz-agent 的 mandatory 扩展packages/shared/src/mandatory-extensions.json,tier: feature)——xyz-agent 启动时自动安装并启用,无需手动操作。

独立 pi 环境手动安装:

npm install @zhushanwen/pi-scheduler

安装后扩展在 pi 会话启动时自动装配(见下节),无需额外配置。

激活方式

扩展 factory(src/index.ts)监听 pi 的 session 生命周期事件,在 session_start 时装配完整调用链:

session_start
  ├─ PiSchedulerBackend(pi.sendMessage + 时间源 + appendEntry + getEntries)
  │    └─ SchedulerRuntime(内存态 + 30s tick 调度 + 限流)
  │         └─ SchedulerService(tool/command 唯一业务入口)
  ├─ runtime.loadTasks(replay(getEntries()))  ← 重放 session JSONL 的 custom entry 折叠恢复任务
  │                                             (仅 ownerSessionFile 匹配的加载,fork 副本被过滤)
  ├─ runtime.startScheduler()                  ← 启动 30s tick
  └─ 注册 scheduler widget(每个 tick 后刷新)

运行时每次状态变更会 append 一条 custom entry 到创建任务的 session 的 JSONL(统一 customType: pi-scheduler:taskop 字段区分):

| op | 触发时机 | 携带数据 | |----|---------|---------| | upsert | 创建 / 更新任务 | task 全快照(含 nextRunAt 初值、ownerSessionFile) | | advance | dispatch 成功后 | 推进后的 nextRunAt、本次执行 at / status | | toggle | 启用 / 停用 | enabled | | delete | 删除;once 触发后自动 delete | taskId |

  • fork 出的 session 重放时按 ownerSessionFile 过滤,不加载、不执行继承的任务副本(原 session resume 照常)
  • session_shutdown 停止 tick + 兜底执行延迟删除 cleanup(正常路径已由首个 turn_end 完成)——任务已 append 到 JSONL,无需额外写盘

/schedule 命令用法

注册为 /schedule 命令。无参数时显示 usage;第一个参数匹配子命令关键词则走子命令分支,否则尝试创建任务。

子命令

| 子命令 | 行为 | |--------|------| | /schedule list | 列出所有任务(id、名称、调度、下次执行时间) | | /schedule on <id> | 启用任务 | | /schedule off <id> | 停用任务(推荐临时暂停用 off,不用 rm) | | /schedule rm <id> | 删除任务 | | /schedule run <id> | 立即执行任务 | | /schedule once <schedule> <prompt> | 创建一次性提醒(kind=once) | | /schedule cron <expression> <prompt> | 创建 cron 任务 |

任务 id 由 8 位 hex 自动生成,list 后从输出中获取。

引号转义

参数用 shell 风格引号解析(tokenizeQuotedsrc/commands.ts):

  • 单引号 '...' 或双引号 "..." 内的内容作为一个 token,引号字符本身被剥离
  • 含空格的多词参数(cron 表达式、prompt)必须加引号,否则会被拆成多个 token

例如 cron 表达式 0 9 * * 1-5 含空格,必须写成 '/schedule cron '0 9 * * 1-5' standup';prompt check build 同理写成 '/schedule 5m 'check build''

子命令补全

输入 /schedule 后 Tab 补全子命令关键词(list/on/off/rm/run/once/cron);on/off/rm/run 之后补全当前任务 id。

schedule 语法

duration(间隔调度)

<数字><单位>,数字与单位间可有空格,大小写不敏感

| 单位 | 含义 | 乘数 | |------|------|------| | s / sec / second / seconds | 秒 | 1,000 ms | | m / min / minute / minutes | 分 | 60,000 ms | | h / hr / hour / hours | 时 | 3,600,000 ms | | d / day / days | 天 | 86,400,000 ms |

例如:5m2h1d30seconds2hours。非法输入(裸数字、未知单位、空串、负值)解析失败 → 创建任务报 INVALID_SCHEDULE

cron(时间点调度)

标准 cron 表达式,支持 5 字段与 6 字段:

  • 5 字段(分 时 日 月 周):自动补 0 秒字段(如 0 9 * * 1-50 0 9 * * 1-5
  • 6 字段(秒 分 时 日 月 周):原样使用

含空格自动走 cron 分支:schedule 输入中不含空格 → 按 duration 解析;含空格 → 按 cron 解析。因此 cron 表达式必须包含空格(正常写法天然如此),duration 不得含空格。

选项语义

schedule tool 的 kind / name / expires / force 参数(/schedule 命令的 once/cron 前缀对应 kind):

| 选项 | 取值 | 语义 | |------|------|------| | kind | recurring(默认)/ once | recurring 每次触发后按 schedule 重算下次时间;once 触发一次后自动删除 | | name | 字符串 | 任务可读名称,缺省从 prompt 自动生成(≤30 字原样,超长截前 27 字加省略号) | | expires | duration 字符串 / never | recurring 任务的过期时间:now + durationnever 永不过期;缺省 7 天。once 任务忽略 expires 参数(触发即删,传不传都不生效) | | force | true / false(默认) | true 时即使 agent 忙(非 idle 或有 pending 消息)也强制 dispatch;false 时忙则延迟到下次 tick |

示例

recurring 间隔任务(每 5 分钟检查构建):

/schedule 5m 'check build'

一次性提醒(10 秒后提醒):

/schedule once 10s remind

cron 任务(工作日早 9 点站会):

/schedule cron '0 9 * * 1-5' standup

force 立即触发(tool 调用:即使忙也执行):

{"prompt": "deploy staging", "schedule": "*/10 * * * *", "force": true}

永不过期(tool 调用:长期 recurring 任务不设 7 天默认过期):

{"prompt": "monthly report", "schedule": "1d", "expires": "never"}

限制与运行时行为

| 限制/行为 | 值 | 说明 | |-----------|-----|------| | 任务上限 | 50MAX_TASKS) | 超过抛 Task limit reached (50),需先删除任务 | | 触发频率上限 | 6 次/分钟RATE_LIMIT_PER_MINUTE) | 滑动 60s 窗口。/schedule run 超限返回 DISPATCH_SKIPPED;tick 自动 dispatch 超限静默跳过 | | tick 间隔 | 30sTICK_INTERVAL_MS) | 到期任务在下一个 tick 被 dispatch;实际触发时间可能比计划晚最多 30s | | 默认过期 | 7 天DEFAULT_EXPIRY_MS) | recurring 任务缺省 expires 时;expires: 'never' 关闭 | | once 任务 | 触发后自动删除 | 不参与后续调度 | | cron 失效 | 任务停用 + lastStatus=failed + lastError='cron expression invalid' | 不会用 now() 兜底导致每 tick 重触发死循环 | | 忙时 dispatch | 非 force 任务在 agent 忙(非 idle / 有 pending 消息)时跳过,延迟到下次 tick | force=true 可绕过 | | history | 保留最近 20 条执行记录 | 超出丢弃最旧;重放折叠时同样裁剪 | | 持久化 | custom entry append 到 session JSONL(dispatch 成功后立即 append advance 记录执行) | 任务随 owner session 持久化,resume 后重放恢复,无需额外写盘 | | 交付语义 | at-least-once(至少一次) | dispatch 成功后内存更新 nextRunAt 并 append advance;append 之前若进程崩溃可能重复注入一次(无精确一次保证,可接受) | | 延迟写入窗口 | 新 session 首 turn 内建任务后进程崩溃可能丢失 | pi 延迟写入:首条 assistant 消息前不 flush。窗口窄、概率极低、无恢复手段 | | 触发条件 | pi 进程需存活且 session 打开 | 电脑睡眠 / pi 进程未运行 = 不触发(非系统 cron,无后台守护) |

错误语义:创建时 schedule 解析失败 → INVALID_SCHEDULErun/toggle/delete 引用不存在的 id → TASK_NOT_FOUNDrun 时任务 disabled / busy / rate-limited → DISPATCH_SKIPPED(message 含 busy, disabled, or rate-limited)。

数据存储位置

当前机制

任务存储为 custom entry,写入创建它的 session 的 JSONL 文件(统一 customType: pi-scheduler:taskop 字段区分 upsert / advance / toggle / delete)。任务物理归属于创建它的 session——appendEntry 把 entry 写入当前 session 的 JSONL,getEntries() 重放折叠恢复内存态。

append-only

custom entry 物理追加到 JSONL,不修改、不删除——pi 依赖 JSONL 物理保留来维持 session tree 的 parentId 链。重放时 per-taskId 按 entry 顺序折叠得到当前态:

  • upsert → 任务以快照覆盖(last-write-wins,含 ownerSessionFile / nextRunAt 初值)
  • advance → 推进 nextRunAt、记录本次执行(at / status)
  • toggle → 切换 enabled
  • delete → 该任务标记消失(once 触发后自动 delete,重放即不见)

末态 = per taskId 最后一个非 delete op 的结果。

不进入 LLM context

pi 的 context 构建对 custom entry 无 case(被过滤)——任务数据零污染对话上下文,不影响 token / 模型上下文。

归属即结构性质

任务物理存在于创建 session 的 JSONL 内。session 文件删除 = 任务消失,无残留、无需 GC、无分片文件。fork 出的 session 不加载继承的任务副本(ownerSessionFile 过滤),subagent 也不受主 session 任务干扰。

旧版迁移

升级前任务存在 cwd 共享的旧 store(~/.pi/agent/scheduler/<cwd>/scheduler.json)。升级后首个检测到旧文件的 session 原子 renamescheduler.json.imported,逐任务 appendEntry upsert 到自己的 JSONL,然后删除 .imported(⚠️ 删除时机依赖 flush:resumed session 已落盘可立即删;新 session(pi 延迟写入,entries 仅内存)延迟到首个 turn_end(该轮 message_end 已全部持久化,flush 必已发生)确认 flush 后删,session_shutdown 兜底;未 flush 保留 .imported 供崩溃恢复重导入,避免源文件销毁 + 数据未落盘的双重丢失):

  • 归属:旧任务无 owner 信息,归属首个完成导入的 session(无更好近似)
  • 过期任务立即触发:导入后若 nextRunAt 已过期,首个 tick 立即 dispatch(once 立即注入、recurring 补跑)

entry 累积

recurring 长期 session 的 scheduler entry 会持续累积(每次 dispatch append 一条 advance)。量级可控:约 100B/条,1h 任务运行一年约 8760 条 ≈ 876KB。且 custom entry 不进 LLM context,不影响 token / 模型上下文。不做物理裁剪(append-only 约束 + advance 是 nextRunAt 正确性的必要记录,不可省)。未来若成问题,方向是等 pi 提供 compaction hook,不是本 extension 自建裁剪。

依赖的 pi 行为清单

本扩展的存储方案(custom entry event sourcing)依赖以下 pi 源码行为。这些是实测存在但非 SDK 契约承诺的隐式行为,pi 升级后需逐条复核:

  1. pi.appendEntry / ctx.sessionManager.getEntries() 存在且 custom entry 不进 LLM context:custom entry 在 pi 的 context 构建(sessionEntryToContextMessages)中无 case,被 flatMap 过滤,任务数据零污染对话上下文。若 pi 未来把 custom entry 纳入 context,会污染 token / 模型输入
  2. fork(forkFrom)全文件复制 custom entry:forkFrom 是全文件复制(含被放弃分支的 entries,无 fork 点概念),不是 fork 点路径复制。本扩展靠 owner 过滤兜底两条复制路径。若 pi 改为按分支选择性复制,fork 隔离逻辑需重新评估
  3. getEntries() 返回全量 entries(不按当前分支过滤):实测 getEntries() 返回全部 fileEntries(session-manager.js:980-982),navigate 只改 leafId 指针不改 entries。因此任务不随 navigate 消失。若 pi 改为按 leafId / 分支过滤 getEntries,切换分支会导致任务丢失
  4. navigate / 切换分支不改任务 entries:navigate 只移动 leafId 指针,不增删 custom entry,任务 entries 跨分支稳定存在。若 pi 未来在 navigate 时裁剪 entries,任务持久性会破坏

任一条行为变更都需重新验证 design 的 D1 / D2 断言与验收场景(尤其 resume、fork 场景)。

开发

pnpm test          # 运行全部 vitest 测试(等价 npx vitest run)
npx vitest run src/__tests__/<file>.test.ts   # 单个文件

测试策略:

  • 依赖反转SchedulerRuntime 只依赖 SchedulerBackend 接口(sendMessage / appendEntry / now),不触碰 FS/pi。测试注入 MockSchedulerBackendsrc/backend.ts 同文件 export)实现零副作用测试
  • 纯函数parseDuration / formatDuration / parseSchedule / computeNextRunAt / computeNextRunssrc/parsing.ts)无副作用,可直接断言
  • 重放折叠:custom entry 折叠协议(upsert / advance / toggle / delete,含 nextRunAt 重放恢复、fork owner 过滤)
  • 旧 store 导入:rename .imported 原子收敛(单成功者、崩溃恢复)

扩展内部结构:backend.ts(后端抽象)→ replay.ts(custom entry 重放折叠)→ runtime.ts(调度核心)→ service.ts(业务入口)→ tool.ts / commands.ts(tool 与 /schedule 命令适配层)→ widget.ts(状态栏 widget)→ importer.ts(旧 store 导入)。