@zhushanwen/pi-scheduler 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -30
- package/package.json +15 -4
- package/skills/scheduler-ext-config/SKILL.md +37 -36
- package/src/__tests__/U4-MODEL-SWITCH.test.ts +638 -0
- package/src/__tests__/ack-contract.test.ts +253 -0
- package/src/__tests__/ack-notify.test.ts +152 -0
- package/src/__tests__/ack-provider.test.ts +288 -0
- package/src/__tests__/ack-turn.test.ts +456 -0
- package/src/__tests__/commands-form.test.ts +415 -0
- package/src/__tests__/commands.test.ts +207 -152
- package/src/__tests__/create-form-component.test.ts +474 -0
- package/src/__tests__/format.test.ts +37 -16
- package/src/__tests__/index-generation.test.ts +17 -1
- package/src/__tests__/index-session-start.test.ts +50 -18
- package/src/__tests__/interaction.test.ts +140 -0
- package/src/__tests__/mock-backend.ts +18 -2
- package/src/__tests__/parsing.test.ts +38 -0
- package/src/__tests__/sdk-contract.test.ts +10 -8
- package/src/__tests__/service.test.ts +251 -4
- package/src/__tests__/tool-create-flow.test.ts +135 -0
- package/src/__tests__/tool.test.ts +7 -5
- package/src/__tests__/widget-push.test.ts +231 -0
- package/src/__tests__/widget.test.ts +237 -17
- package/src/ack-notify.ts +56 -0
- package/src/ack-provider.ts +250 -0
- package/src/ack-turn.ts +372 -0
- package/src/backend.ts +73 -4
- package/src/commands.ts +153 -91
- package/src/create-form-component.ts +1010 -0
- package/src/format.ts +75 -30
- package/src/i18n.ts +458 -0
- package/src/importer.ts +2 -2
- package/src/index.ts +188 -16
- package/src/interaction.ts +304 -0
- package/src/runtime.ts +394 -4
- package/src/service.ts +96 -25
- package/src/tool.ts +65 -11
- package/src/types.ts +113 -0
- package/src/widget.ts +133 -18
- package/vitest.config.ts +2 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# scheduler
|
|
2
2
|
|
|
3
|
-
定时任务调度扩展:按 duration(`5m` / `2h` / `1d`)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once
|
|
3
|
+
定时任务调度扩展:按 duration(`5m` / `2h` / `1d`)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once)与过期策略(expires)。创建入口两路:**人侧 `/schedule` 命令打开创建表单**(表单异步打开,填表时长不受命令通道超时约束),**模型侧 `schedule` tool 直建**(不再弹确认表单;参数不完整时必须先向用户澄清——见「创建方式」)。任务随 owner session 持久化,resume 后继续触发。
|
|
4
4
|
|
|
5
5
|
## 产品定位
|
|
6
6
|
|
|
@@ -14,11 +14,11 @@ pi-scheduler 是 **session 存活期间的 AI 提醒器**——在 pi 进程运
|
|
|
14
14
|
|
|
15
15
|
**任务归属创建它的 session**:任务物理存储在创建它的 session 的 JSONL 文件内,**只在 owner session 打开(继续对话 / resume)时才触发**。
|
|
16
16
|
|
|
17
|
-
> **这是设计决策,不是 bug(D9
|
|
17
|
+
> **这是设计决策,不是 bug(D9)**:本扩展定位是 **session 级 AI 提醒器**——任务归属创建它的 session,只在 owner session 存活时触发。如果你每天开新 session,昨天建的"明早检查 CI"任务今天不会响,除非你 resume 昨天创建该任务的那个 session。这不是"任务丢了":任务随 session 持久化,session 不打开就不调度。**用户若发现「昨天建的任务今天没响」,这是预期行为**,请 resume 创建该任务的 session。
|
|
18
18
|
|
|
19
19
|
## 简介与安装
|
|
20
20
|
|
|
21
|
-
pi-scheduler 是
|
|
21
|
+
pi-scheduler 是 taiji 的 **mandatory 扩展**(`packages/shared/src/mandatory-extensions.json`,tier: `feature`)——taiji 启动时自动安装并启用,无需手动操作。
|
|
22
22
|
|
|
23
23
|
独立 pi 环境手动安装:
|
|
24
24
|
|
|
@@ -49,7 +49,7 @@ session_start
|
|
|
49
49
|
|----|---------|---------|
|
|
50
50
|
| `upsert` | 创建 / 更新任务 | task 全快照(含 nextRunAt 初值、ownerSessionFile) |
|
|
51
51
|
| `advance` | dispatch 成功后 | 推进后的 nextRunAt、本次执行 at / status |
|
|
52
|
-
| `toggle` | 启用 / 停用 | enabled |
|
|
52
|
+
| `toggle` | 启用 / 停用 | enabled(enable 时若重算了 nextRunAt 则随 op 一并携带,防 resume 回退到过期值) |
|
|
53
53
|
| `delete` | 删除;once 触发后自动 delete | taskId |
|
|
54
54
|
|
|
55
55
|
- fork 出的 session 重放时按 `ownerSessionFile` 过滤,不加载、不执行继承的任务副本(原 session resume 照常)
|
|
@@ -57,21 +57,21 @@ session_start
|
|
|
57
57
|
|
|
58
58
|
## /schedule 命令用法
|
|
59
59
|
|
|
60
|
-
注册为 `/schedule
|
|
60
|
+
注册为 `/schedule`。无参数 → 打开创建表单(空草稿,默认 `循环 + 每天 09:00`);带参 `<schedule> <prompt>` → 打开预填表单;第一个参数匹配子命令关键词则走子命令分支。命令 handler **异步打开表单后立即返回**——用户在表单里停留多久都不会触发命令通道的 60s 超时(会话级提醒器不该因为「用户想了两分钟」而报错)。
|
|
61
61
|
|
|
62
62
|
### 子命令
|
|
63
63
|
|
|
64
64
|
| 子命令 | 行为 |
|
|
65
65
|
|--------|------|
|
|
66
|
+
| `/schedule` | 打开创建表单(空草稿;默认循环 + 每天 09:00) |
|
|
67
|
+
| `/schedule <schedule> <prompt>` | 打开预填时间/提示词的创建表单(如 `/schedule 5m 'check build'`) |
|
|
66
68
|
| `/schedule list` | 列出所有任务(id、名称、调度、下次执行时间) |
|
|
67
69
|
| `/schedule on <id>` | 启用任务 |
|
|
68
70
|
| `/schedule off <id>` | 停用任务(推荐临时暂停用 off,不用 rm) |
|
|
69
71
|
| `/schedule rm <id>` | 删除任务 |
|
|
70
72
|
| `/schedule run <id>` | 立即执行任务 |
|
|
71
|
-
| `/schedule once <schedule> <prompt>` | 创建一次性提醒(kind=once) |
|
|
72
|
-
| `/schedule cron <expression> <prompt>` | 创建 cron 任务 |
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
一次性(once)与 cron 的形态由**表单内**选择/填写(执行模式切换 + 自定义 cron 输入),不再有 `once` / `cron` 关键字子命令。任务 id 由 8 位 hex 自动生成,`list` 后从输出中获取。
|
|
75
75
|
|
|
76
76
|
### 引号转义
|
|
77
77
|
|
|
@@ -80,11 +80,11 @@ session_start
|
|
|
80
80
|
- 单引号 `'...'` 或双引号 `"..."` 内的内容作为一个 token,引号字符本身被剥离
|
|
81
81
|
- 含空格的多词参数(cron 表达式、prompt)**必须加引号**,否则会被拆成多个 token
|
|
82
82
|
|
|
83
|
-
例如
|
|
83
|
+
例如 prompt `check build` 写成 `/schedule 5m 'check build'`(预填表单的提示词字段)。引号只包住含空格的那个参数本身,不要在外层再套引号(嵌套引号会拆出错误 token)。
|
|
84
84
|
|
|
85
85
|
### 子命令补全
|
|
86
86
|
|
|
87
|
-
输入 `/schedule ` 后 Tab 补全子命令关键词(list/on/off/rm/run
|
|
87
|
+
输入 `/schedule ` 后 Tab 补全子命令关键词(list/on/off/rm/run);`on`/`off`/`rm`/`run` 之后补全当前任务 id。
|
|
88
88
|
|
|
89
89
|
## schedule 语法
|
|
90
90
|
|
|
@@ -99,7 +99,7 @@ session_start
|
|
|
99
99
|
| `h` / `hr` / `hour` / `hours` | 时 | 3,600,000 ms |
|
|
100
100
|
| `d` / `day` / `days` | 天 | 86,400,000 ms |
|
|
101
101
|
|
|
102
|
-
例如:`5m`、`2h`、`1d`、`30seconds`、`2hours
|
|
102
|
+
例如:`5m`、`2h`、`1d`、`30seconds`、`2hours`。非法输入(裸数字、未知单位、空串、负值)解析失败:`schedule` tool 在预校验即报错(`Invalid parameters: unrecognized schedule "..."`,修正参数后重调);`/schedule` 命令在 `json`/`print` 模式带参直建时报 `Invalid schedule: "..."`(rpc/tui 模式则由表单提交后报错)。
|
|
103
103
|
|
|
104
104
|
### cron(时间点调度)
|
|
105
105
|
|
|
@@ -112,42 +112,77 @@ session_start
|
|
|
112
112
|
|
|
113
113
|
## 选项语义
|
|
114
114
|
|
|
115
|
-
`schedule`
|
|
115
|
+
任务创建与管理由两个 tool 承担:`schedule`(`prompt` / `schedule` / `kind` / `name` / `expires` / `model`)与 `schedule_control`(`action`: list / toggle / delete / run,附 `id` / `enabled`)。`schedule` 的参数即**创建参数**:调用直接创建任务,无确认表单(见「创建方式」)。下表为 `schedule` tool 的选项语义(`/schedule` 表单的 once / cron 形态对应 kind / 自定义 cron):
|
|
116
116
|
|
|
117
117
|
| 选项 | 取值 | 语义 |
|
|
118
118
|
|------|------|------|
|
|
119
119
|
| `kind` | `recurring`(默认)/ `once` | recurring 每次触发后按 schedule 重算下次时间;once 触发一次后自动删除 |
|
|
120
120
|
| `name` | 字符串 | 任务可读名称,缺省从 prompt 自动生成(≤30 字原样,超长截前 27 字加省略号) |
|
|
121
121
|
| `expires` | duration 字符串 / `never` | recurring 任务的过期时间:`now + duration`;`never` 永不过期;缺省 7 天。**once 任务忽略 expires 参数**(触发即删,传不传都不生效) |
|
|
122
|
-
| `
|
|
122
|
+
| `model` | scoped model id(`provider/model`) | 任务执行所用模型;缺省跟随会话当前模型 |
|
|
123
|
+
|
|
124
|
+
## 创建方式(触发反转)
|
|
125
|
+
|
|
126
|
+
两条路径,职责不同:
|
|
127
|
+
|
|
128
|
+
**人侧 `/schedule`**:打开创建表单(GUI 由 FormOverlay 的 ScheduleForm 渲染,TUI 由 `ScheduleCreateComponent`),用户在表单里自选模式/时间/模型/提示词后创建。命令 handler **异步打开表单后立即返回**——填表时长不受命令通道超时约束。模式矩阵:
|
|
129
|
+
|
|
130
|
+
| 形态 | 会话模式 | 交互 |
|
|
131
|
+
|------|---------|------|
|
|
132
|
+
| GUI 表单 | rpc | 统一表单协议(`uiFormInteract` + marker select 通道):FormOverlay 弹出 schedule 表单,预填草稿经 initial 直传;命令立即返回,交互在回包窗口之外续跑 |
|
|
133
|
+
| TUI 表单 | tui | `ctx.ui.custom` 挂 `ScheduleCreateComponent`:模式 → 时间 → 模型 → 提示词 → 提交 五 tab 逐项确认,两段 Esc 取消 |
|
|
134
|
+
| 非交互 | json / print | 不做交互:带参 `<schedule> <prompt>` → 直建(成功看 session entry,`/schedule list` 可验)。无参或带参失败(非法表达式 / 缺 prompt)→ **抛错**(该模式 `ctx.ui.notify` 是 no-op、handler 返回值被丢弃,stderr 是唯一可见通道) |
|
|
135
|
+
|
|
136
|
+
**模型侧 `schedule` tool**:直接创建(无确认表单、无 headless「未经确认」附注)。参数不完整时必须**先经 ask-user / 对话澄清**,禁止猜测频率后静默创建(`promptGuidelines` 硬性条目)。创建成功的 tool result 含任务 id 与后续运行时刻,模型应复述给用户以便核对/撤销。
|
|
137
|
+
|
|
138
|
+
表单通道/协议的失败折叠(命令路径):
|
|
139
|
+
|
|
140
|
+
| 态 | 触发 | 结果 |
|
|
141
|
+
|----|------|------|
|
|
142
|
+
| cancelled | 用户取消表单 | 不创建、无 toast(取消不是错误) |
|
|
143
|
+
| channel-error | RPC 交互通道不可用:select reject,或回包回显请求 payload(宿主不识别表单协议的旧组合) | `ctx.ui.notify(..., 'error')` 提示通道不可用 / 升级宿主;**本会话后续 `/schedule` 直接给同样提示,不重复试探** |
|
|
144
|
+
| non-json | 回包形状非法(非协议 JSON / 非 ScheduleFormResult)= 协议版本错配 | `ctx.ui.notify(..., 'error')` 提示协议版本错配 |
|
|
145
|
+
| abort | `session_shutdown`(quit/reload/new/resume/fork)→ 命令路径自持 AbortController abort | 不创建、无 toast(taiji 内切换会话不触发,表单保留且仍有效) |
|
|
146
|
+
|
|
147
|
+
取消/超时不是错误:不创建、无 toast。命令路径的错误出口统一按 mode 分流——rpc / tui 走 `notify(..., 'error')`(pi 会把 handler 异常折成 renderer 不消费的 `extension.error`,throw 在 rpc 下静默丢弃);json / print 一律 `rethrow`。
|
|
148
|
+
|
|
149
|
+
### 文案与语言(L2 本地化)
|
|
150
|
+
|
|
151
|
+
命令反馈(创建/列表/启停/删除/用法/错误提示)与 TUI widget 文本、托盘标题均由扩展侧词典(`src/i18n.ts`)按当前界面语言渲染;语言经 `<dataDir>/ui-preferences.json`(runtime 写、扩展只读,见 data-source-registry 登记行)就地读取,文件缺失/损坏回落 `en-US`。`/schedule` 的 registerCommand.description 是注册期静态串、托盘标题随 widget 刷新(≤30s)跟进语言——两者切语言后不会立即热更(已接受滞后)。
|
|
152
|
+
|
|
153
|
+
模型可见文案(`schedule` tool 的 `description` / `promptGuidelines` / tool result)**保持英文**,不经 L2 词典——两受众不串(本地化不得污染 tool result)。
|
|
123
154
|
|
|
124
155
|
## 示例
|
|
125
156
|
|
|
126
|
-
**recurring
|
|
157
|
+
**recurring 间隔任务**(`/schedule` 打开预填表单,确认后创建):
|
|
127
158
|
|
|
128
159
|
```
|
|
129
160
|
/schedule 5m 'check build'
|
|
130
161
|
```
|
|
131
162
|
|
|
132
|
-
|
|
163
|
+
**交互创建**(无参打开空表单,表单里选每天 09:00):
|
|
133
164
|
|
|
134
165
|
```
|
|
135
|
-
/schedule
|
|
166
|
+
/schedule
|
|
136
167
|
```
|
|
137
168
|
|
|
138
|
-
|
|
169
|
+
**一次性提醒**:在 `/schedule` 表单里把执行模式切到「一次性」并选时刻。
|
|
170
|
+
|
|
171
|
+
**cron 任务**:在 `/schedule` 表单里选自定义 cron 并填表达式(如 `0 9 * * 1-5`)。
|
|
172
|
+
|
|
173
|
+
**非交互创建**(json/print 模式,无表单可开,带参直建):
|
|
139
174
|
|
|
140
175
|
```
|
|
141
|
-
/schedule
|
|
176
|
+
pi -p "/schedule 5m 'check build'"
|
|
142
177
|
```
|
|
143
178
|
|
|
144
|
-
|
|
179
|
+
**立即执行**(schedule_control tool 调用:马上 dispatch 现有任务):
|
|
145
180
|
|
|
146
181
|
```json
|
|
147
|
-
{"
|
|
182
|
+
{"action": "run", "id": "<task-id>"}
|
|
148
183
|
```
|
|
149
184
|
|
|
150
|
-
|
|
185
|
+
**永不过期**(`schedule` tool 调用:直接创建长期 recurring 任务):
|
|
151
186
|
|
|
152
187
|
```json
|
|
153
188
|
{"prompt": "monthly report", "schedule": "1d", "expires": "never"}
|
|
@@ -158,19 +193,19 @@ session_start
|
|
|
158
193
|
| 限制/行为 | 值 | 说明 |
|
|
159
194
|
|-----------|-----|------|
|
|
160
195
|
| 任务上限 | **50**(`MAX_TASKS`) | 超过抛 `Task limit reached (50)`,需先删除任务 |
|
|
161
|
-
| 触发频率上限 | **6 次/分钟**(`RATE_LIMIT_PER_MINUTE`) | 滑动 60s 窗口。`/schedule run` 超限返回
|
|
196
|
+
| 触发频率上限 | **6 次/分钟**(`RATE_LIMIT_PER_MINUTE`) | 滑动 60s 窗口。`/schedule run` 超限返回 not dispatched(disabled, rate-limited, or dispatch in flight);tick 自动 dispatch 超限静默跳过 |
|
|
162
197
|
| tick 间隔 | **30s**(`TICK_INTERVAL_MS`) | 到期任务在下一个 tick 被 dispatch;实际触发时间可能比计划晚最多 30s |
|
|
163
198
|
| 默认过期 | **7 天**(`DEFAULT_EXPIRY_MS`) | recurring 任务缺省 `expires` 时;`expires: 'never'` 关闭 |
|
|
164
199
|
| once 任务 | 触发后自动删除 | 不参与后续调度 |
|
|
165
200
|
| cron 失效 | 任务停用 + `lastStatus=failed` + `lastError='cron expression invalid'` | 不会用 `now()` 兜底导致每 tick 重触发死循环 |
|
|
166
|
-
| 忙时 dispatch |
|
|
201
|
+
| 忙时 dispatch | steer 直投(scheduler-steer-direct-dispatch) | busy 时消息插入当前 turn、idle 时开新 turn(`{deliverAs:'steer', triggerTurn:true}`),受理即记账不排队;同任务 in-flight 守卫防双投 |
|
|
167
202
|
| history | 保留最近 **20** 条执行记录 | 超出丢弃最旧;重放折叠时同样裁剪 |
|
|
168
203
|
| 持久化 | custom entry append 到 session JSONL(dispatch 成功后立即 append advance 记录执行) | 任务随 owner session 持久化,resume 后重放恢复,无需额外写盘 |
|
|
169
204
|
| 交付语义 | **at-least-once**(至少一次) | dispatch 成功后内存更新 nextRunAt 并 append advance;append 之前若进程崩溃可能重复注入一次(无精确一次保证,可接受) |
|
|
170
205
|
| 延迟写入窗口 | 新 session 首 turn 内建任务后进程崩溃可能丢失 | pi 延迟写入:首条 assistant 消息前不 flush。窗口窄、概率极低、无恢复手段 |
|
|
171
206
|
| 触发条件 | pi 进程需存活且 session 打开 | 电脑睡眠 / pi 进程未运行 = 不触发(非系统 cron,无后台守护) |
|
|
172
207
|
|
|
173
|
-
|
|
208
|
+
错误语义(无错误码;双受众分派——`message` 英文回退供 tool result / `messageKey`+`params` 供命令层词典渲染,见「文案与语言」):`schedule` tool 创建时参数预校验失败(prompt 空 / schedule 非法)→ 直接 throw `Invalid parameters: ...`(修正参数后重调);`/schedule` 命令 `json`/`print` 模式带参解析失败 → throw 词典文案(en: `Invalid schedule: "..."`);`run`/`toggle`/`delete` 引用不存在的 id / `run` 时任务 disabled / rate-limited / 同任务在途 / 任务数超上限 → 命令层 toast 按词典本地化(en 示例:`Task <id> not found` / `Task <id> not dispatched (disabled, rate-limited, or dispatch in flight)` / `Task limit reached (50) — delete one first`)。表单交互的取消/abort 不是错误(不创建、无 toast);通道失败/回包非法在 rpc/tui 走 `notify(..., 'error')`、在 json/print 走 throw——见「创建方式」。
|
|
174
209
|
|
|
175
210
|
## 数据存储位置
|
|
176
211
|
|
|
@@ -187,7 +222,7 @@ custom entry 物理追加到 JSONL,不修改、不删除——pi 依赖 JSONL
|
|
|
187
222
|
- `toggle` → 切换 enabled
|
|
188
223
|
- `delete` → 该任务标记消失(once 触发后自动 delete,重放即不见)
|
|
189
224
|
|
|
190
|
-
末态 = per taskId
|
|
225
|
+
末态 = per taskId 全序列折叠的结果:最后一次 delete 之后若无后续 upsert 则任务不存在;advance / toggle 为增量 op,叠加在其 upsert 快照之上。
|
|
191
226
|
|
|
192
227
|
### 不进入 LLM context
|
|
193
228
|
|
|
@@ -199,14 +234,14 @@ pi 的 context 构建对 custom entry 无 case(被过滤)——任务数据
|
|
|
199
234
|
|
|
200
235
|
### 旧版迁移
|
|
201
236
|
|
|
202
|
-
升级前任务存在 cwd 共享的旧 store(`~/.pi/agent/
|
|
237
|
+
升级前任务存在 cwd 共享的旧 store(`~/.pi/agent/schedule/` 下按 cwd 路径展开的 `scheduler.json`;导入时同时探测 `getAgentDir()` 下的同形路径)。升级后首个检测到旧文件的 session 原子 `rename` 为 `scheduler.json.imported`,逐任务 appendEntry upsert 到自己的 JSONL,然后删除 `.imported`(⚠️ 删除时机依赖 flush:resumed session 已落盘可立即删;新 session(pi 延迟写入,entries 仅内存)延迟到首个 `turn_end`(该轮 message_end 已全部持久化,flush 必已发生)确认 flush 后删,`session_shutdown` 兜底;未 flush 保留 `.imported` 供崩溃恢复重导入,避免源文件销毁 + 数据未落盘的双重丢失):
|
|
203
238
|
|
|
204
239
|
- **归属**:旧任务无 owner 信息,**归属首个完成导入的 session**(无更好近似)
|
|
205
240
|
- **过期任务立即触发**:导入后若 nextRunAt 已过期,**首个 tick 立即 dispatch**(once 立即注入、recurring 补跑)
|
|
206
241
|
|
|
207
242
|
### entry 累积
|
|
208
243
|
|
|
209
|
-
recurring 长期 session 的 scheduler entry 会持续累积(每次 dispatch append 一条 advance)。量级可控:约
|
|
244
|
+
recurring 长期 session 的 scheduler entry 会持续累积(每次 dispatch append 一条 advance)。量级可控:约 250B/条,1h 任务运行一年约 8760 条 ≈ 2MB。且 custom entry 不进 LLM context,不影响 token / 模型上下文。**不做物理裁剪**(append-only 约束 + advance 是 nextRunAt 正确性的必要记录,不可省)。未来若成问题,方向是等 pi 提供 compaction hook,不是本 extension 自建裁剪。
|
|
210
245
|
|
|
211
246
|
## 依赖的 pi 行为清单
|
|
212
247
|
|
|
@@ -214,7 +249,7 @@ recurring 长期 session 的 scheduler entry 会持续累积(每次 dispatch a
|
|
|
214
249
|
|
|
215
250
|
1. **`pi.appendEntry` / `ctx.sessionManager.getEntries()` 存在且 custom entry 不进 LLM context**:custom entry 在 pi 的 context 构建(`sessionEntryToContextMessages`)中无 case,被 flatMap 过滤,任务数据零污染对话上下文。若 pi 未来把 custom entry 纳入 context,会污染 token / 模型输入
|
|
216
251
|
2. **fork(`forkFrom`)全文件复制 custom entry**:forkFrom 是全文件复制(含被放弃分支的 entries,无 fork 点概念),不是 fork 点路径复制。本扩展靠 owner 过滤兜底两条复制路径。若 pi 改为按分支选择性复制,fork 隔离逻辑需重新评估
|
|
217
|
-
3. **`getEntries()` 返回全量 entries(不按当前分支过滤)**:实测 `getEntries()` 返回全部 fileEntries(session-manager.js:
|
|
252
|
+
3. **`getEntries()` 返回全量 entries(不按当前分支过滤)**:实测 `getEntries()` 返回全部 fileEntries(session-manager.js:982-984),navigate 只改 leafId 指针不改 entries。因此任务不随 navigate 消失。若 pi 改为按 leafId / 分支过滤 getEntries,切换分支会导致任务丢失
|
|
218
253
|
4. **navigate / 切换分支不改任务 entries**:navigate 只移动 leafId 指针,不增删 custom entry,任务 entries 跨分支稳定存在。若 pi 未来在 navigate 时裁剪 entries,任务持久性会破坏
|
|
219
254
|
|
|
220
255
|
任一条行为变更都需重新验证 design 的 D1 / D2 断言与验收场景(尤其 resume、fork 场景)。
|
|
@@ -228,9 +263,10 @@ npx vitest run src/__tests__/<file>.test.ts # 单个文件
|
|
|
228
263
|
|
|
229
264
|
测试策略:
|
|
230
265
|
|
|
231
|
-
- **依赖反转**:`SchedulerRuntime` 只依赖 `SchedulerBackend` 接口(`sendMessage` / `appendEntry` / `now`),不触碰 FS/pi。测试注入 `MockSchedulerBackend`(`src/__tests__/mock-backend.ts
|
|
266
|
+
- **依赖反转**:`SchedulerRuntime` 只依赖 `SchedulerBackend` 接口(`sendMessage` / `appendEntry` / `now`),不触碰 FS/pi。测试注入 `MockSchedulerBackend`(`src/__tests__/mock-backend.ts` 测试专用实现)实现零副作用测试
|
|
232
267
|
- **纯函数**:`parseDuration` / `formatDuration` / `parseSchedule` / `computeNextRunAt` / `computeNextRuns`(`src/parsing.ts`)无副作用,可直接断言
|
|
233
268
|
- **重放折叠**:custom entry 折叠协议(upsert / advance / toggle / delete,含 nextRunAt 重放恢复、fork owner 过滤)
|
|
269
|
+
- **直建流**:`handleSchedule` 预校验 / abort / 参数透传(`src/__tests__/tool-create-flow.test.ts`);命令路径表单(无参 / 带参预填 / 取消 / 提交 / channel-error / non-json / echo / 模式矩阵 / 异步不阻塞 / json·print throw,`src/__tests__/commands-form.test.ts`);交互生命周期(草稿构造 + AbortController 注册表,`src/__tests__/interaction.test.ts`)
|
|
234
270
|
- **旧 store 导入**:rename `.imported` 原子收敛(单成功者、崩溃恢复)
|
|
235
271
|
|
|
236
|
-
扩展内部结构:`backend.ts`(后端抽象)→ `replay.ts`(custom entry 重放折叠)→ `runtime.ts`(调度核心)→ `service.ts`(业务入口)→ `tool.ts` / `
|
|
272
|
+
扩展内部结构:`backend.ts`(后端抽象)→ `replay.ts`(custom entry 重放折叠)→ `runtime.ts`(调度核心)→ `service.ts`(业务入口)→ `tool.ts`(tool 直建流)/ `commands.ts`(/schedule 命令适配层)→ `interaction.ts`(表单交互 / 协议错配 / AbortController 生命周期)→ `create-form-component.ts`(TUI 创建表单组件,由 `interaction.ts` 消费)→ `widget.ts`(双模状态栏 widget:GUI 结构化 meta + TUI 文本行)→ `i18n.ts`(L2 词典 / locale 读取 / 结果渲染)→ `importer.ts`(旧 store 导入)。
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-scheduler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
-
"
|
|
6
|
+
"taiji": {
|
|
7
7
|
"role": "universal"
|
|
8
8
|
},
|
|
9
9
|
"pi": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"pi-package"
|
|
19
19
|
],
|
|
20
20
|
"devDependencies": {
|
|
21
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
21
22
|
"@vitest/coverage-v8": "^4.1.9",
|
|
22
23
|
"fast-check": "^4.9.0",
|
|
23
24
|
"vitest": "^4.1.8"
|
|
@@ -29,21 +30,31 @@
|
|
|
29
30
|
"vitest.config.ts"
|
|
30
31
|
],
|
|
31
32
|
"peerDependencies": {
|
|
33
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
32
34
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
35
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
33
36
|
"typebox": "*"
|
|
34
37
|
},
|
|
35
38
|
"peerDependenciesMeta": {
|
|
39
|
+
"@earendil-works/pi-ai": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
36
42
|
"@earendil-works/pi-coding-agent": {
|
|
37
43
|
"optional": true
|
|
38
44
|
},
|
|
45
|
+
"@earendil-works/pi-tui": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
39
48
|
"typebox": {
|
|
40
49
|
"optional": true
|
|
41
50
|
}
|
|
42
51
|
},
|
|
43
52
|
"dependencies": {
|
|
44
53
|
"croner": "^9.0.0",
|
|
45
|
-
"@zhushanwen/
|
|
46
|
-
"@zhushanwen/pi-
|
|
54
|
+
"@zhushanwen/extension-protocol": "0.13.0",
|
|
55
|
+
"@zhushanwen/pi-ext-guards": "0.4.1",
|
|
56
|
+
"@zhushanwen/pi-extension-logger": "0.6.1",
|
|
57
|
+
"@zhushanwen/pi-llm-shared": "0.9.0"
|
|
47
58
|
},
|
|
48
59
|
"scripts": {
|
|
49
60
|
"test": "vitest run",
|
|
@@ -5,7 +5,7 @@ description: "使用或排查 @zhushanwen/pi-scheduler(定时任务调度)
|
|
|
5
5
|
|
|
6
6
|
# scheduler 使用与存储指南
|
|
7
7
|
|
|
8
|
-
> @zhushanwen/pi-scheduler:定时任务调度扩展。任务到期时向当前 session 注入一条 message(`deliverAs: '
|
|
8
|
+
> @zhushanwen/pi-scheduler:定时任务调度扩展。任务到期时向当前 session 注入一条 message(`deliverAs: 'steer'` + `triggerTurn: true`,steer 直投:busy 时插入当前 turn、idle 时开新 turn),唤醒 agent 处理。
|
|
9
9
|
|
|
10
10
|
**重要前提**:scheduler **没有独立的配置文件**。任务通过命令/工具交互创建,数据以 append-only event sourcing 方式存储在 session JSONL 中(见下文「数据存储位置」)。排查「任务存哪 / 为什么 resume 后任务变了 / fork 后任务是否继承」都必须基于此模型理解,不要去找独立的 `scheduler.json`(那是已废弃的旧版格式,仅迁移探测时使用)。
|
|
11
11
|
|
|
@@ -13,29 +13,35 @@ description: "使用或排查 @zhushanwen/pi-scheduler(定时任务调度)
|
|
|
13
13
|
|
|
14
14
|
两条入口,底层都走 `SchedulerService`(单一业务实现,无双轨):
|
|
15
15
|
|
|
16
|
-
### 1. `/schedule` slash
|
|
16
|
+
### 1. `/schedule` slash 命令(用户直接输入)
|
|
17
17
|
|
|
18
18
|
| 用法 | 作用 |
|
|
19
19
|
|------|------|
|
|
20
|
-
| `/schedule
|
|
21
|
-
| `/schedule
|
|
22
|
-
| `/schedule cron '<cron表达式>' <prompt>` | 创建 cron 任务(**必须用引号**包裹,否则空格会被 tokenize 拆散) |
|
|
20
|
+
| `/schedule` | 打开创建表单(空草稿,默认循环 + 每天 09:00) |
|
|
21
|
+
| `/schedule <schedule> <prompt>` | 打开预填时间/提示词的创建表单(如 `/schedule 5m 'check build'`) |
|
|
23
22
|
| `/schedule list` | 列出全部任务(按 nextRunAt 排序) |
|
|
24
23
|
| `/schedule on <id>` / `/schedule off <id>` | 启用 / 禁用某任务 |
|
|
25
24
|
| `/schedule rm <id>` | 删除某任务 |
|
|
26
25
|
| `/schedule run <id>` | 立即触发一次某任务 |
|
|
27
26
|
|
|
28
|
-
-
|
|
27
|
+
- 一次性(once)与 cron 形态由表单内选择/填写,不再有 `once` / `cron` 关键字子命令。
|
|
28
|
+
- 命令 handler 异步打开表单后立即返回——填表时长不受命令通道超时约束。
|
|
29
29
|
- `on`/`off`/`rm`/`run` 的 `<id>` 支持命令补全(`getArgumentCompletions` 会列出 `id · name · schedule`)。
|
|
30
|
-
-
|
|
30
|
+
- `json`/`print` 模式无交互通道:带参直建,无参/失败抛错(stderr 可见)。
|
|
31
31
|
|
|
32
32
|
### 2. `schedule` / `schedule_control` 工具(AI 调用)
|
|
33
33
|
|
|
34
|
-
- **`schedule`**(创建):参数 `prompt`(必填,到期注入的消息)、`schedule`(必填,duration 或 cron)、`kind`(`once`/`recurring`,默认 `recurring`)、`name`(可选,缺省从 prompt 自动截取前 30 字)、`expires`(可选,默认 7 天;传 `"never"` 关闭过期)、`
|
|
34
|
+
- **`schedule`**(创建):参数 `prompt`(必填,到期注入的消息)、`schedule`(必填,duration 或 cron)、`kind`(`once`/`recurring`,默认 `recurring`)、`name`(可选,缺省从 prompt 自动截取前 30 字)、`expires`(可选,默认 7 天;传 `"never"` 关闭过期)、`model`(可选,scoped model id(`provider/model`),任务执行所用模型;缺省跟随会话当前模型)。
|
|
35
35
|
- **`schedule_control`**(管理):`action` = `list`/`toggle`/`delete`/`run`,`id`(toggle/delete/run 必填),`enabled`(toggle 必填)。
|
|
36
36
|
- 两个工具的返回都是结构化 `{content: [{type:'text', text}], details}`;业务失败以异常抛出(pi 只对 execute throw 置 `isError:true`,错误 message 作为 toolResult content 返回),不通过返回值表达失败。
|
|
37
37
|
|
|
38
|
-
>
|
|
38
|
+
> 创建/管理操作不受 agent 运行状态约束;**到期 dispatch** 也不等待 agent idle(steer 直投,busy 时插入当前 turn),仅受速率限制/同任务 in-flight 守卫约束(见「运行限制与 dispatch 行为」)。
|
|
39
|
+
|
|
40
|
+
### 3. 创建方式(触发反转)
|
|
41
|
+
|
|
42
|
+
- **模型侧 `schedule` tool**:**直接创建**(无确认表单、无 headless「未经确认」附注)。参数不完整时必须先经 ask-user / 对话澄清,禁止猜测频率后静默创建。
|
|
43
|
+
- **人侧 `/schedule` 命令**:打开创建表单(GUI 走统一表单协议 / TUI 走 `ScheduleCreateComponent`),用户在表单里自定模式/时间/模型/提示词;命令异步打开、立即返回。`json`/`print` 模式无交互通道:带参直建、无参或失败抛错。
|
|
44
|
+
- **会话生命周期**:表单挂起期间 `session_shutdown`(quit/reload/new/resume/fork)会 abort 交互(不创建);taiji 内切换会话不触发,表单保留。
|
|
39
45
|
|
|
40
46
|
## 调度格式
|
|
41
47
|
|
|
@@ -72,7 +78,7 @@ description: "使用或排查 @zhushanwen/pi-scheduler(定时任务调度)
|
|
|
72
78
|
- 调用 `pi.appendEntry('pi-scheduler:task', op)`,`customType` 固定为 `pi-scheduler:task`。
|
|
73
79
|
- op 有四种:`upsert`(创建,携带全量 `TaskSnapshot`)、`advance`(recurring dispatch 成功后推进 `nextRunAt`)、`toggle`(启用/禁用)、`delete`(删除 / once 执行后 / 过期清理)。
|
|
74
80
|
- session 启动时(`session_start` 事件),`PiSchedulerBackend.loadTasks()` 调 `replayFoldEntries` 折叠当前 session 的全部 `pi-scheduler:task` custom entries,重放出当前任务状态。**append-only 不做全量 persist**——没有「保存」动作,每次操作即时 append。
|
|
75
|
-
- 因此「任务存哪」的答案是:**创建它的那个 session 的 JSONL 文件**。该文件位于 pi agent 目录下(`getAgentDir()` 读 `PI_CODING_AGENT_DIR`,默认 `~/.pi/agent`;
|
|
81
|
+
- 因此「任务存哪」的答案是:**创建它的那个 session 的 JSONL 文件**。该文件位于 pi agent 目录下(`getAgentDir()` 读 `PI_CODING_AGENT_DIR`,默认 `~/.pi/agent`;taiji 数据目录隔离时指向隔离目录如 `~/.taiji/...`)。
|
|
76
82
|
|
|
77
83
|
### owner 隔离(fork 行为)
|
|
78
84
|
|
|
@@ -89,17 +95,17 @@ description: "使用或排查 @zhushanwen/pi-scheduler(定时任务调度)
|
|
|
89
95
|
旧版(npm ≤ 0.1.1)用独立 store 文件,按 **cwd 隔离**存储:
|
|
90
96
|
|
|
91
97
|
```
|
|
92
|
-
<agentDir>/
|
|
98
|
+
<agentDir>/schedule/<root>/<segments>/schedule.json
|
|
93
99
|
```
|
|
94
100
|
|
|
95
101
|
- `<agentDir>` = `getAgentDir()`(候选 1)或 `~/.pi/agent`(候选 2,旧版硬编码)。
|
|
96
102
|
- `<root>` = cwd 根盘符 sanitize:mac/linux 的 `/` → `root`;Windows `C:\` → `c`(非字母数字转 `-`,trim 首尾,小写)。
|
|
97
103
|
- `<segments>` = cwd 去根盘符后的路径段,按 `path.sep` 拆分。例 cwd `/Users/foo/project` → `Users/foo/project`。
|
|
98
|
-
- 完整示例(mac):`~/.pi/agent/
|
|
104
|
+
- 完整示例(mac):`~/.pi/agent/schedule/root/Users/foo/project/schedule.json`。
|
|
99
105
|
|
|
100
106
|
**迁移机制**(`importLegacyStore`,session_start 时自动执行,无需用户介入):
|
|
101
107
|
|
|
102
|
-
1. 双候选探测:优先 `getAgentDir()` 路径,不存在则 fallback `~/.pi/agent/
|
|
108
|
+
1. 双候选探测:优先 `getAgentDir()` 路径,不存在则 fallback `~/.pi/agent/schedule/...`(兼容 taiji 数据目录隔离前的旧版写入位置)。
|
|
103
109
|
2. 原子 rename `scheduler.json` → `scheduler.json.imported` 独占迁移;rename 抛 ENOENT 说明并发/崩溃已被别人处理,走 `.imported` 残留恢复。
|
|
104
110
|
3. 读取 `.imported`,逐任务 `appendEntry('pi-scheduler:task', upsert)` 写入当前 session(owner 归属当前 session)。
|
|
105
111
|
4. 删除 `.imported`:**新 session 首次 flush 前(尚未收到 assistant 消息)延迟删除**,由首个 `turn_end` / `session_shutdown` 确认 flush 后再删(防未 flush 即退出导致任务永久丢失 + 源文件已毁)。
|
|
@@ -117,12 +123,14 @@ description: "使用或排查 @zhushanwen/pi-scheduler(定时任务调度)
|
|
|
117
123
|
| 默认过期 | 7 天(`DEFAULT_EXPIRY_MS`) | 仅 recurring;`expires="never"` 关闭 |
|
|
118
124
|
| 历史记录 | 最近 20 条(`HISTORY_LIMIT`) | 每任务的执行历史 |
|
|
119
125
|
|
|
120
|
-
dispatch
|
|
126
|
+
dispatch 行为(`dispatchTask`,scheduler-steer-direct-dispatch 直投模型):
|
|
121
127
|
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
128
|
+
- **steer 直投**:到期任务 `backend.sendMessage(..., {deliverAs: 'steer', triggerTurn: true})` 直接投递——agent busy 时消息插入当前 turn(立即被模型看到)、idle 时开新 turn。不排队、不等 idle、不丢弃。
|
|
129
|
+
- **受理即记账**:`sendMessage` 是 fire-and-forget(返回 void),await 立即通过,`nextRunAt` 调用即推进——无「入队未终态」窗口。
|
|
130
|
+
- **同任务 in-flight 守卫**:该任务 dispatch 在途(如 `/schedule run` 与 tick dispatch 并发)时本 tick 跳过,防双投。
|
|
131
|
+
- **模型切换**(`task.model` 设定且 ≠ 会话当前模型):sendMessage 前 `setModel` 切到目标任务模型、记未决切换记录,事件恢复(turn_end + isIdle 复核)后切回原模型;切换失败降级——照常 dispatch(模型字段不阻塞核心调度)。
|
|
132
|
+
- dispatch 成功后:recurring 推进 `nextRunAt` 并 append `advance`;once 删除任务并 append `delete`;失败(`sendMessage` 同步抛错,如 session 关闭)记 `lastStatus='failed'` 不 rethrow,下个 tick 重试(transient 失败重试语义,不 append advance)。
|
|
133
|
+
- 注入的消息:`{content: task.prompt, customType: 'pi-scheduler:dispatched', display: true}`,投递选项 `{deliverAs: 'steer', triggerTurn: true}`。
|
|
126
134
|
|
|
127
135
|
## 任务数据结构
|
|
128
136
|
|
|
@@ -133,8 +141,8 @@ dispatch 触发条件(`dispatchTask`):
|
|
|
133
141
|
- `prompt`:到期注入的 message 内容。
|
|
134
142
|
- `kind`:`once` | `recurring`。
|
|
135
143
|
- `schedule`:`{mode:'cron', cronExpression}` | `{mode:'interval', intervalMs}`。
|
|
144
|
+
- `model?`:任务执行模型(scoped model id,`provider/model`);缺省跟随会话当前模型。
|
|
136
145
|
- `enabled`:是否启用。
|
|
137
|
-
- `force`:是否在 agent busy 时强制 dispatch。
|
|
138
146
|
- `createdAt` / `nextRunAt` / `expiresAt?`:时间戳(ms)。
|
|
139
147
|
- `runCount` / `lastRunAt?` / `lastStatus?`(`success`|`failed`)/ `lastError?`:执行统计。
|
|
140
148
|
- `history`:最近 20 条 `ExecutionRecord`(`{at, status}`)。
|
|
@@ -145,30 +153,23 @@ dispatch 触发条件(`dispatchTask`):
|
|
|
145
153
|
|
|
146
154
|
## 示例
|
|
147
155
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
/schedule 5m 检查当前项目的构建状态,失败则报告原因
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
创建 2 小时后的一次性提醒:
|
|
156
|
+
打开创建表单(空草稿,表单里选每天 09:00):
|
|
155
157
|
|
|
156
158
|
```
|
|
157
|
-
/schedule
|
|
159
|
+
/schedule
|
|
158
160
|
```
|
|
159
161
|
|
|
160
|
-
|
|
162
|
+
打开预填表单(每 5 分钟检查构建状态):
|
|
161
163
|
|
|
162
164
|
```
|
|
163
|
-
/schedule
|
|
165
|
+
/schedule 5m 检查当前项目的构建状态,失败则报告原因
|
|
164
166
|
```
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
一次性提醒 / cron 任务:在 `/schedule` 表单里切换执行模式(一次性)或填自定义 cron(如 `*/30 * * * *`)。非交互模式(json/print)带参直建:
|
|
167
169
|
|
|
168
170
|
```
|
|
169
|
-
/schedule
|
|
171
|
+
pi -p "/schedule 5m 'check build'"
|
|
170
172
|
```
|
|
171
|
-
(如需 force + 不过期,用 `schedule` 工具传 `force:true, expires:"never"`,命令行暂未暴露这两个开关)
|
|
172
173
|
|
|
173
174
|
列出并禁用某任务:
|
|
174
175
|
|
|
@@ -177,15 +178,15 @@ dispatch 触发条件(`dispatchTask`):
|
|
|
177
178
|
/schedule off abc12345
|
|
178
179
|
```
|
|
179
180
|
|
|
180
|
-
AI
|
|
181
|
+
AI 通过工具创建(指定模型 + 永不过期;工具直建,无确认表单):
|
|
181
182
|
|
|
182
183
|
```
|
|
183
|
-
schedule({ prompt: "...", schedule: "1h", kind: "recurring",
|
|
184
|
+
schedule({ prompt: "...", schedule: "1h", kind: "recurring", model: "anthropic/claude-sonnet-4-5", expires: "never", name: "hourly-check" })
|
|
184
185
|
```
|
|
185
186
|
|
|
186
187
|
## 备注
|
|
187
188
|
|
|
188
189
|
- **croner 依赖**:cron 模式依赖 `croner`(dependencies,随包自动安装 + 静态 import,2026-09 0.6.0 起;此前为 optional peer,独立安装形态下 cron 任务会静默失败)。cron 解析失败(`INVALID_SCHEDULE`)= 表达式无效,不存在「解析器未安装」形态。
|
|
189
|
-
- **数据目录隔离**:任务存储在 `getAgentDir()` 指向的 session JSONL。
|
|
190
|
+
- **数据目录隔离**:任务存储在 `getAgentDir()` 指向的 session JSONL。taiji 通过 `TAIJI_AGENT_DATA_DIR` / `PI_CODING_AGENT_DIR` 隔离实例时,任务随 session 落在隔离目录,与 `~/.pi/agent` 互不干扰。
|
|
190
191
|
- **无配置 schema 可编辑**:scheduler 的所有状态都由运行时命令产生,没有可手动编辑的配置文件。要「批量预置任务」只能在 session 内逐条创建(或迁移旧 store)。
|
|
191
|
-
-
|
|
192
|
+
- **无参 `/schedule`**:打开创建表单(不是提示语)。任务管理用 `list`/`on`/`off`/`rm`/`run` 子命令或 `schedule_control` 工具。
|