@x-otto/schedule 0.0.1-alpha.2 → 0.0.1-alpha.4

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/dist/index.d.ts CHANGED
@@ -1,6 +1,23 @@
1
1
  import { Migration, Persistence } from "@x-otto/persistence";
2
2
  import { AgentJobRegistry } from "@x-otto/runtime";
3
3
 
4
+ //#region ../interchange/dist/index.d.ts
5
+ //#endregion
6
+ //#region src/model.d.ts
7
+ /**
8
+ * 推理投入档(reasoning effort)——RFC-235 对齐 Claude Code 词表:`low/medium/high/xhigh/max`。
9
+ * 去除历史遗留的 `minimal`(无对应厂商语义,存量配置载入时迁移为 `low`),新增 `max`(Claude
10
+ * Code 最高档 + DeepSeek `reasoning_effort:'max'` 顶档)。各模型真实支持的子集由 manifest 的
11
+ * `Model.thinkingLevels` 声明,provider 组装请求体时按"≤该档最高支持档"降级(见 RFC-235 §3.3)。
12
+ */
13
+ type ThinkingLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
14
+ /**
15
+ * 模型能力优势标签(供大模型为 subagent 选型)。非 /models API 返回——由
16
+ * model-capabilities 据「精选家族图 + 启发式」推导。
17
+ * planning=方案/架构 · knowledge=知识/通识 · coding=编码 · reasoning=深度推理
18
+ * · vision=视觉 · speed=低延迟 · long-context=长上下文
19
+ */
20
+ //#endregion
4
21
  //#region src/types.d.ts
5
22
  /** A scheduled task persisted to ${OTTO_HOME}/schedules/<id>.json */
6
23
  interface ScheduledTask {
@@ -49,6 +66,37 @@ interface ScheduledTask {
49
66
  * 时不使用。
50
67
  */
51
68
  boundSessionId?: string;
69
+ /**
70
+ * RFC-423 D2:事件触发描述。存在时任务由进程内事件驱动而非 cron 驱动
71
+ * (cronExpression 被忽略,任务不进 tick 的 findDue)。缺省 = cron 驱动(既有语义,零变化)。
72
+ */
73
+ eventTrigger?: EventTriggerSpec;
74
+ /**
75
+ * RFC-438:到点执行时使用的模型 id(缺省 = 当前会话模型/默认)。
76
+ * 只存 id 字符串——modelRegistry 是唯一真源,schedule 不持有 Model 对象。
77
+ */
78
+ modelId?: string;
79
+ /** RFC-438:思考档位(配 modelId 使用)。缺省 = provider 默认(detached-run 实际 ?? 'medium')。 */
80
+ thinkingLevel?: ThinkingLevel;
81
+ }
82
+ /**
83
+ * RFC-423 D2:事件触发描述。
84
+ *
85
+ * MVP 只支持进程内事件源(source: 'job',监听 AgentJobRegistry 终态)。
86
+ * git commit / file watch 等进程外源列为后续 RFC,架构预留 source 开放为 string。
87
+ */
88
+ interface EventTriggerSpec {
89
+ /** 事件源类型(MVP:'job'——AgentJobRegistry 终态事件) */
90
+ source: string;
91
+ /** 事件名(source='job' 时为 AgentJobStatus:'applied' | 'failed' | 'canceled') */
92
+ event: string;
93
+ /**
94
+ * 可选过滤器(如只对特定 sessionId 的 job 触发)。死循环闸门之一(D4):
95
+ * 缺省不过滤 sessionId 时,闸门2(默认拒绝 job 投递)+ debounce(闸门3)承担防护。
96
+ */
97
+ filter?: {
98
+ sessionId?: string;
99
+ };
52
100
  }
53
101
  /** RFC-229 M1: structured result from SchedulerService.cancel() */
54
102
  interface CancelResult {
@@ -67,7 +115,7 @@ interface CancelResult {
67
115
  * 整套调度设施,正是 RFC 列为"放弃方案"的路径。
68
116
  */
69
117
  interface ScheduleSubscriber {
70
- kind: 'plugin-service';
118
+ kind: string;
71
119
  pluginId: string;
72
120
  serviceId: string;
73
121
  }
@@ -86,6 +134,12 @@ interface ScheduledTaskInput {
86
134
  target?: 'job' | 'session';
87
135
  /** RFC-357 D2:`target==='session'` 时必填,调用方(scheduler.add())从当前 sessionRef 取值填充。 */
88
136
  boundSessionId?: string;
137
+ /** RFC-423 D2:事件触发描述。存在时任务由进程内事件驱动,cronExpression 仅作占位。 */
138
+ eventTrigger?: EventTriggerSpec;
139
+ /** RFC-438:到点执行时使用的模型 id(缺省 = 当前会话模型/默认)。 */
140
+ modelId?: string;
141
+ /** RFC-438:思考档位(配 modelId 使用)。 */
142
+ thinkingLevel?: ThinkingLevel;
89
143
  }
90
144
  //#endregion
91
145
  //#region src/registry.d.ts
@@ -101,35 +155,16 @@ declare class ScheduleRegistry {
101
155
  * Find tasks that are due for execution.
102
156
  * Returns all enabled tasks (recurring or one-shot) whose nextFireAt <= now.
103
157
  * The 'firing' guard is a scheduler concern (managed via a separate Set).
158
+ *
159
+ * RFC-423:eventTrigger 任务永不由时间"到点"——它们由进程内事件驱动(InProcessEventTrigger
160
+ * arm 监听),不进 cron 轮询。此处排除,使 cron 轮询(tick)与事件监听(arm)
161
+ * 两条路径互斥(D5)。
104
162
  */
105
163
  findDue(now: number): ScheduledTask[];
106
164
  count(): number;
107
165
  has(id: string): boolean;
108
166
  }
109
167
  //#endregion
110
- //#region src/cron-parser.d.ts
111
- /**
112
- * Parse a 5-field cron expression (minute hour dom month dow) and return the next
113
- * fire time >= from (inclusive). All times in local timezone.
114
- *
115
- * Throws on invalid expressions.
116
- *
117
- * Field syntax: wildcard (*), single value (5), step (e.g. \*\/15 or 5\/15),
118
- * range (1-5), list (1,15,30), range with step (1-10\/2).
119
- *
120
- * Unsupported: L, W, ?, #, name aliases (MON, JAN).
121
- *
122
- * ## DoM∧DoW semantics
123
- *
124
- * If both day-of-month and day-of-week are constrained (not a bare *),
125
- * a date matches if **EITHER** field matches (standard vixie-cron OR behaviour).
126
- *
127
- * ## Day-of-week mapping
128
- *
129
- * 0 = Sunday, 1–6 = Monday–Saturday, 7 = also Sunday.
130
- */
131
- declare function parseCron(expr: string, from?: Date): Date;
132
- //#endregion
133
168
  //#region src/schedule-store.d.ts
134
169
  interface ScheduleStore {
135
170
  saveTask(task: ScheduledTask): Promise<void>;
@@ -159,6 +194,18 @@ interface FireOwnership {
159
194
  renew(): Promise<void>;
160
195
  release(): Promise<void>;
161
196
  lastFailureReason?(): 'occupied' | 'network-error' | undefined;
197
+ /**
198
+ * 当前锁持有者的可读描述(诊断用,非语义判定)。返回 `undefined` 表示无法确定
199
+ * (旧格式锁文件、锁已消失、或远程实现——服务端仲裁没有本地 pid 概念)。
200
+ *
201
+ * 为什么是可选端口:`FireOwnership` 是本地 O_EXCL 与远程 HTTP 双实现的公共接口
202
+ * (RFC-169 D3/D3b),"谁持有"只在本地实现里有确切答案。消费方必须按可选处理,
203
+ * 拿不到就退回不带主体的提示文案,不得据此做任何准入判定。
204
+ */
205
+ describeOwner?(): {
206
+ pid?: number;
207
+ host?: string;
208
+ } | undefined;
162
209
  /** Whether this process currently holds the lock (verified against the token). */
163
210
  isOwner(): boolean;
164
211
  }
@@ -168,327 +215,474 @@ interface FireOwnership {
168
215
  */
169
216
  declare function createLocalFireOwnership(workspaceKey: string): FireOwnership;
170
217
  //#endregion
171
- //#region src/schedule-lease-migrations.d.ts
172
- declare const SCHEDULE_LEASE_MIGRATIONS: Migration[];
218
+ //#region src/schedule-config.d.ts
219
+ /**
220
+ * schedule-config.ts —— RFC-406 D4:常量配置化。
221
+ *
222
+ * 之前散落在 scheduler.ts 顶部的硬编码常量,提取为可注入的配置接口。
223
+ * 不同部署场景(单用户 vs 团队共享、高频 vs 低频)可调整参数,不需改源码。
224
+ *
225
+ * **不纳入本接口的常量**(刻意游离,非遗漏):
226
+ * - `fire-ownership.ts` 的 `STALE_MS`(5 分钟)——本地锁过期阈值,有不变量约束
227
+ * (必须超过 tick renew 间隔,否则锁在 renew 周期间被误判过期),调错会致
228
+ * 多进程重复 fire。它是 fire-ownership 的领域常量,不是可调运维参数。
229
+ * - `remote-fire-ownership.ts` 的 `HEARTBEAT_MS`(30s)/ `STALE_MS`(180s)——
230
+ * 远程租约心跳与过期,有 1:6 比例约束(过期 > 6 倍心跳,防网络抖动误夺锁)。
231
+ * 调错同样致多终端重复 fire 或锁饥饿。两者已通过模块 export 暴露
232
+ * (`REMOTE_FIRE_OWNERSHIP_HEARTBEAT_MS` / `REMOTE_FIRE_OWNERSHIP_STALE_MS`),
233
+ * 不进 ScheduleConfig 以免暗示可随意调。
234
+ */
235
+ interface ScheduleConfig {
236
+ /** 最大任务数 */
237
+ maxTasks: number;
238
+ /** recurring 任务过期阈值(超过此时间未触发则清除) */
239
+ recurringEvictionMs: number;
240
+ /** recurring 任务 jitter 窗口 */
241
+ recurringJitterWindowMs: number;
242
+ /** one-shot 任务 jitter 窗口 */
243
+ oneShotJitterWindowMs: number;
244
+ /** 默认单次执行超时 */
245
+ defaultMaxDurationMs: number;
246
+ /** 默认重试次数 */
247
+ defaultMaxRetries: number;
248
+ /** ticker 间隔 */
249
+ tickIntervalMs: number;
250
+ /** needs_input 硬上限倍数 */
251
+ needsInputMaxExtensionFactor: number;
252
+ /** 每任务最大 fire log 条数 */
253
+ maxFireLogsPerTask: number;
254
+ /** owner 从 store 重新同步的间隔(tick 数) */
255
+ reloadIntervalTicks: number;
256
+ /** recovery 模式重试间隔 */
257
+ recoveryRetryIntervalMs: number;
258
+ }
259
+ declare const DEFAULT_SCHEDULE_CONFIG: ScheduleConfig;
173
260
  //#endregion
174
- //#region src/remote-schedule-store.d.ts
175
- interface RemoteScheduleStoreOptions {
261
+ //#region src/fire-target.d.ts
262
+ interface FireTarget {
263
+ /** 唯一标识('job' | 'session' | 'plugin-service') */
264
+ readonly type: string;
265
+ /** 判断此 handler 是否能处理该 task。注册顺序决定优先级——先注册的先匹配。 */
266
+ canHandle(task: ScheduledTask): boolean;
176
267
  /**
177
- * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸
178
- * origin,该值本身是完整资源路径(对齐 `otto serve` 后端约定)。内部用
179
- * `new URL(sessionUrl).origin` 提取协议+主机+端口,再拼接 `/api/storage/schedules`——
180
- * 与 `remote-fire-ownership.ts` 的同一提取逻辑保持一致,不能直接拿 sessionUrl 当前缀用。
268
+ * 执行投递。返回 FireResult——scheduler 不关心 handler 内部如何实现,
269
+ * 只根据 outcome 决定后续行为(one-shot 清理 / recurring 记账)。
181
270
  */
182
- sessionUrl: string;
183
- getAuth: () => Promise<{
184
- token: string;
185
- }>;
186
- fetch?: typeof globalThis.fetch;
187
- timeoutMs?: number;
188
- wsKey?: string | (() => string | undefined);
271
+ fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult>;
272
+ /**
273
+ * fire() 退出保障钩子(M1 修复①):无论 fire 正常返回、抛错还是早退,executor 的
274
+ * finally 块都会调用。handler 必须在此清理本次 fire 的运行态记账
275
+ * (runningJobs / cancelRequested 等)——漏清会导致 cancel 意图残留、
276
+ * recurring 任务被永久跳过、运行态 Map 无界泄漏。
277
+ */
278
+ cleanup?(taskId: string): void;
279
+ /**
280
+ * 可选能力:支持取消正在运行的投递(job 路径实现)。
281
+ * SchedulerService.cancel() 路由到当前 fire 该任务的 handler。
282
+ */
283
+ requestCancel?(taskId: string): CancelResult;
284
+ /** 可选能力:当前有并发运行态的 handler(job 路径)暴露运行中的 task id 集合。 */
285
+ getRunningTaskIds?(): Set<string>;
286
+ /**
287
+ * 可选能力:session 投递 handler 暴露"创建任务时绑定的当前 sessionId"端口,
288
+ * scheduler.add() 经此填充 boundSessionId(RFC-406 D6)。
289
+ */
290
+ getSessionId?(): string;
291
+ }
292
+ interface FireContext {
293
+ /** 本次计划触发时刻(ticker 路径传入,runNow 用实际时刻)。用于 occurrenceId 派生。 */
294
+ scheduledFor?: number;
295
+ now: () => number;
296
+ logger?: {
297
+ error: (msg: string | Error, ...args: unknown[]) => void;
298
+ };
299
+ }
300
+ interface FireResult {
301
+ outcome: 'success' | 'failed';
302
+ /** cancel 信号——JobFireTarget 在 cancel() 路径设置 */
303
+ cancelled?: boolean;
304
+ error?: string;
305
+ /** job-specific 信号(JobFireTarget 附加,用于 runningJobsByTaskId 记账) */
306
+ jobId?: string;
189
307
  }
308
+ //#endregion
309
+ //#region src/trigger.d.ts
190
310
  /**
191
- * Create a ScheduleStore backed by the persistenced `/api/storage/schedules` namespace's
192
- * generic storage route (`storage.ts`, `namespace='schedules'`) distinct from the
193
- * lease sub-route (`schedule-lease.ts`, `/api/schedules/...`) which handles fire-ownership
194
- * arbitration only.
311
+ * 事件监听型触发器。`arm()` 建立订阅、返回退订函数——退订由
312
+ * `SchedulerService.terminate()` 咽喉点统一调用(→ RFC-433 §4 R2),
313
+ * 实现方不必自行管理生命周期。
195
314
  */
196
- declare function createRemoteScheduleStore(options: RemoteScheduleStoreOptions): ScheduleStore;
197
- //#endregion
198
- //#region src/remote-fire-ownership.d.ts
199
- interface RemoteFireOwnershipOptions {
315
+ interface EventDrivenTrigger {
316
+ /** 唯一标识(当前仅 `'in-process-event'`)。 */
317
+ readonly type: string;
318
+ /** 判断此 Trigger 是否负责该任务的触发。 */
319
+ canHandle(task: ScheduledTask): boolean;
200
320
  /**
201
- * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸
202
- * origin。该值本身是完整资源路径(对齐 `otto serve` 后端约定,见 RFC-146 §1.3 两条远程
203
- * 产品线对照表),本构造函数内部用 `new URL(sessionUrl).origin` 提取协议+主机+端口,
204
- * 再拼接 `/api/schedules/...` lease 路径——不能直接拿 `sessionUrl` 当 baseUrl 使用。
321
+ * 为一个任务建立事件订阅。触发条件满足时调用 `fire(task)`。
322
+ * 返回退订函数——`SchedulerService` 在任务终结、stop、重新 arm 时调用,防监听泄漏。
205
323
  */
206
- sessionUrl: string;
207
- wsKey: string;
208
- getAuth: () => Promise<{
209
- token: string;
210
- }>;
211
- fetch?: typeof globalThis.fetch;
212
- timeoutMs?: number;
213
- now?: () => number;
324
+ arm(task: ScheduledTask, fire: (task: ScheduledTask) => void): () => void;
214
325
  }
215
- /** Create a server-arbitrated FireOwnership backed by persistenced's lease endpoints. */
216
- declare function createRemoteFireOwnership(options: RemoteFireOwnershipOptions): FireOwnership;
217
326
  //#endregion
218
327
  //#region src/scheduler.d.ts
328
+ /**
329
+ * RFC-433 D1:任务终结的来源。仅用于诊断/日志语义——终结动作本身对三者一致
330
+ * (退订 → 删快照 → 删 registry),区分来源是为了让调用点自我说明。
331
+ */
332
+ type TerminationReason = /** 用户经 `/schedule remove`、面板或 `schedule_delete` 工具删除。 */'user-removed' /** one-shot 任务成功执行完毕(FireExecutor 终态处理)。 */ | 'one-shot-complete' /** recurring cron 任务 7 天未触发,被 TickLoop 判为滞留清除。 */ | 'stale-evicted';
219
333
  interface SchedulerDeps {
220
334
  registry: ScheduleRegistry;
221
335
  store: ScheduleStore;
222
336
  ownership: FireOwnership;
223
- jobRegistry: AgentJobRegistry;
224
337
  /**
225
- * Dispatch an agent job. Mirrors AgentJobService.start() shape:
226
- * synchronously returns a record with at least { id }.
227
- */
228
- startJob: (input: {
229
- title: string;
230
- prompt: string;
231
- sessionId: string;
232
- origin: 'user' | 'main';
233
- }) => {
234
- id: string;
235
- };
236
- getSessionId: () => string;
237
- /**
238
- * RFC-327 D2:向订阅者投递到点通知的窄端口(缺省 = 不支持订阅者,此类任务 fire 时
239
- * 记日志跳过而非崩溃)。
338
+ * FireTarget handler 列表。内置 handler(job/session/subscriber)由消费方构造并传入,
339
+ * SchedulerService 不自动注册——消费方完全控制 handler 集合和优先级。
240
340
  *
241
- * 返回是否真的送达——`false` 表示服务未运行/已停机。scheduler 据此写 fire 日志,
242
- * 但**不重试、不报错**:定时通知是尽力而为的旁路信号(at-least-once 的"至少"由
243
- * 下一次 cron 周期保证,不是靠即时重试)。
244
- *
245
- * 端口刻意只接受纯数据标识:scheduler 不知道插件服务的存在形式,也不持有其句柄。
341
+ * 如果传入空数组,fire() 会找不到 handler 并记日志跳过——不会崩溃。
246
342
  */
247
- notifySubscriber?: (subscriber: ScheduleSubscriber, tick: {
248
- scheduleId: string;
249
- occurrenceId: string;
250
- occurredAt: number;
251
- }) => boolean;
343
+ fireTargets: FireTarget[];
252
344
  /**
253
- * RFC-357 D1/D2:`target==='session'` 任务到点后的投递端口——把 `prompt` 注入
254
- * `boundSessionId` 对应的当前会话(经 `AgentSession.prompt(text,{streamingBehavior:'followUp'})`,
255
- * busy 时排队、idle 时直接起新回合)。缺省 = 不支持 session 投递(此类任务 fire 时记日志
256
- * 跳过而非崩溃,同 `notifySubscriber` 缺省行为的纹理)。
257
- *
258
- * 返回值语义(调用方——CLI wiring——必须自行捕获全部失效原因,统一收敛为 boolean):
259
- * `true` = 已成功注入;`false` = 失效(会话身份不匹配/已销毁/archived/paused 等),
260
- * scheduler 不重试、不报错——同 notifySubscriber 一样是尽力而为的旁路信号。
345
+ * RFC-423:事件驱动 Trigger 列表(如 InProcessEventTrigger)。缺省空数组——只有 cron
346
+ * 触发(cron 任务由 TickLoop 轮询驱动,无需消费方传入)。消费方传入事件 trigger
347
+ * 即启用事件触发能力。
261
348
  */
262
- followUpCurrentSession?: (task: {
263
- prompt: string;
264
- boundSessionId: string;
265
- name: string;
266
- origin: 'user' | 'model';
267
- }) => boolean;
349
+ triggers?: EventDrivenTrigger[];
350
+ /** 可选配置覆盖(常量配置化,RFC-406 D4) */
351
+ config?: Partial<ScheduleConfig>;
268
352
  now?: () => number;
353
+ /**
354
+ * `debug` 可选——生产装配(`interactive-schedule.ts`)注入的 logger 只有 `error`;
355
+ * 有 `debug` 的实现能拿到任务终结的来源(user-removed / one-shot-complete /
356
+ * stale-evicted),排查"任务怎么没了"时不必猜。
357
+ */
269
358
  logger?: {
270
359
  error: (msg: string | Error, ...args: unknown[]) => void;
360
+ debug?: (msg: string, ...args: unknown[]) => void;
271
361
  };
272
- /**
273
- * RFC-230:任务失败重试之间的固定延迟(ms)注入端口。缺省真实 `setTimeout`,测试可注入假
274
- * 实现避免真实等待。`retryBackoffMs`(见下)为 0 时仍会调用(`sleep(0)`,一次微任务调度
275
- * 点而非跳过——与当前"循环内无 await 直接进下一轮"存在细微时序差异,但不影响任何可观察
276
- * 行为,见 RFC-230 §3 D6 Scheduler 小节)。
277
- */
278
- sleep?: (ms: number) => Promise<void>;
279
- /**
280
- * RFC-230:每次重试之间的固定延迟(ms)。默认 0(保持现状:不改变既有 per-task 重试
281
- * 节奏,除非显式配置非零值)。真实值来自 `app.getResilienceConfig().schedule.retryBackoffMs`,
282
- * 由真实构造点(`packages/cli/src/commands/interactive-schedule.ts`)注入——不是
283
- * `packages/coding` App 内部构造 `SchedulerService`(二轮评审 P2 修正的装配点误解)。
284
- */
285
- retryBackoffMs?: number;
286
362
  }
287
363
  declare class SchedulerService {
288
364
  private readonly deps;
289
365
  private readonly registry;
290
- private ticker;
366
+ private readonly persistence;
367
+ private readonly fireExecutor;
368
+ private readonly fireLogger;
369
+ private readonly tickLoop;
370
+ private readonly config;
291
371
  private recoveryTimer;
292
- private readonly firing;
293
- private tickInProgress;
372
+ /** RFC-435 D4:从未当过 owner 的非 owner 进程的 acquire 重试循环(TUI 释放锁后本进程可接管)。 */
373
+ private ensureOwnershipTimer;
294
374
  private running;
295
- private tickCount;
296
- private readonly runningJobsByTaskId;
297
- private readonly cancelRequested;
298
- private _nextFireToken;
375
+ /** RFC-423:事件驱动 trigger 列表(消费方传入)。 */
376
+ private readonly triggers;
377
+ /** RFC-423:已 arm 的事件任务 → 退订函数(remove/stop 时调用,防监听泄漏)。 */
378
+ private readonly armedTriggers;
299
379
  constructor(deps: SchedulerDeps);
300
- /**
301
- * RFC-169 D3d: whether this process currently holds fire ownership (ticker actively
302
- * fires due tasks) vs read-only (loaded the task list but a different live process —
303
- * local: another workspace-scoped otto instance; remote: another terminal sharing the
304
- * same wsKey — is the one that will actually fire). Consumed by `/schedule list` to show
305
- * a "read-only, another terminal holds fire ownership" hint (RFC-169 D3d TUI indicator).
306
- */
307
380
  isFireOwner(): boolean;
308
- /**
309
- * F2 (RFC-169 终局 review 2026-07-15): when `isFireOwner()` is false, why. `undefined`
310
- * from the underlying `FireOwnership` (e.g. the local O_EXCL implementation, which never
311
- * has a "can't reach the lock" failure mode — fs ops either succeed or throw) means the
312
- * distinction genuinely doesn't apply; callers should fall back to the generic
313
- * "another terminal holds it" wording. Only the remote implementation can return
314
- * `'network-error'`.
315
- */
316
381
  fireOwnerFailureReason(): 'occupied' | 'network-error' | undefined;
317
382
  /** Load persisted tasks and start the ticker (if fire owner). */
318
383
  start(): Promise<void>;
319
384
  /**
320
- * review S5: incremental re-sync from the store — add tasks the owner doesn't know about
321
- * yet (created by a non-owner process), remove tasks that vanished from the store
322
- * (deleted by a non-owner process), and pick up field edits (enable/disable, cron, prompt)
323
- * made elsewhere. Never touches a task currently `firing` (avoid clobbering in-flight
324
- * dispatch state) and never re-applies jitter to tasks the owner already knows about
325
- * (their nextFireAt is the owner's own scheduling decision, not the store's).
385
+ * 任务终结的**唯一入口**(RFC-433 D1 / R2)。三条删除路径——用户 `remove()`、
386
+ * one-shot 完成、7 天滞留驱逐——全部经此,否则事件监听会泄漏:任务从 registry
387
+ * 消失后,`armedTriggers` 里的退订函数仍在,而 arm 回调闭包捕获的是 **task 对象
388
+ * 引用本身**(见 `armIfEventDriven`),registry 里有没有它根本不影响 fire。
389
+ *
390
+ * **三步顺序不可调换(R2b 不变量)**:`persistence.deleteOne()` 内部要
391
+ * `registry.get(id)` 判 session-local(`schedule-persistence.ts:37`)——registry
392
+ * 先删会让它读不到 task、判不出 session-local,对本就不落盘的任务白发一次 store IO。
326
393
  */
327
- private reloadFromStore;
328
- /** Clear ticker, release ownership, and persist current state. */
329
- stop(): Promise<void>;
394
+ private terminate;
330
395
  /**
331
- * N1 (RFC-169 终局复审 2026-07-23): when S4 detects ownership loss mid-session, stop
332
- * the ticker (prevents double-fire) but enter recovery mode — a low-frequency
333
- * re-acquire loop that can reclaim ownership when a network partition heals or the
334
- * competing process exits. Without this, a single transient heartbeat failure in
335
- * remote mode permanently degrades the process to read-only (contradicting D3d's
336
- * auto-recovery promise) and local suspend-then-resume recovery requires a full
337
- * process restart.
396
+ * 只从内存移除、**保留** store 快照——`reloadFromStore` 的差量同步用(store
397
+ * 本来就已经没有它了,是别的进程删的;再发一次 delete 是多余 IO)。
398
+ * 同样必须退订,理由同 `terminate()`。
338
399
  */
400
+ private evictFromMemory;
401
+ /** Clear ticker, release ownership, and persist current state. */
402
+ stop(): Promise<void>;
339
403
  private startRecovery;
340
404
  private recoveryAttempt;
341
405
  private stopRecovery;
342
406
  /**
343
- * Create a scheduled task.
344
- *
345
- * - Generates an 8-char id via crypto.randomUUID()
346
- * - Parses the cron expression to compute nextFireAt + jitter
347
- * - Enforces a max of 50 tasks (throws if exceeded)
407
+ * owner 进程的低频 acquire 重试。TUI 先开持锁 → daemon 后起(acquire 失败,
408
+ * 本循环待机)→ TUI 关闭释放锁 → 本循环在下一次 tick 接管。锁的原子性
409
+ * (O_EXCL / 服务端 CAS)保证任一时刻只有一个 owner——重试不会造成双主。
348
410
  */
411
+ private startEnsureOwnership;
412
+ private ensureOwnershipAttempt;
413
+ private stopEnsureOwnership;
349
414
  add(input: ScheduledTaskInput): ScheduledTask;
350
- /** Remove a task by id. Returns true if the task existed and was removed. */
351
415
  remove(id: string): boolean;
352
- /**
353
- * Update mutable fields of a task.
354
- *
355
- * If cronExpression changes, nextFireAt is recalculated with jitter.
356
- * Fields not in the patch are left unchanged.
357
- */
358
416
  update(id: string, patch: Partial<Pick<ScheduledTask, 'name' | 'prompt' | 'cronExpression' | 'enabled'>>): boolean;
359
- /** Return all tasks, ordered by nextFireAt ascending. */
360
417
  list(): ScheduledTask[];
361
- /**
362
- * RFC-229 M2: return the set of task IDs that currently have a running job.
363
- * Used by the plugin panel to show [R] status indicators.
364
- */
365
418
  getRunningTaskIds(): Set<string>;
366
- /**
367
- * RFC-229 M3: read the most recent fire log entries for a task.
368
- * Returns up to `maxEntries` lines, newest first. Each entry is { timestamp, status }.
369
- */
370
419
  readFireLogs(taskId: string, maxEntries?: number): Array<{
371
420
  timestamp: string;
372
421
  status: string;
373
422
  }>;
423
+ runNow(id: string): void;
424
+ cancel(taskId: string): CancelResult;
374
425
  /**
375
- * Force-execute a task immediately, regardless of its nextFireAt.
376
- * Does NOT advance nextFireAt this is an extra execution.
426
+ * 若任务是事件驱动(eventTrigger 存在),用匹配的 EventDrivenTrigger arm 监听。
427
+ * fire 回调复用既有 fireExecutor 路径——Trigger 只决定"何时",FireTarget 决定"投哪"。
377
428
  *
378
- * review S3: guarded against a concurrent tick-driven fire() for the same task — without
379
- * this, a manual runNow() while tick's fire() await is in flight (fire() only adds to
380
- * `firing` synchronously before awaiting startJob) could dispatch two jobs for one task.
429
+ * 死循环闸门2(D4):事件驱动任务默认拒绝 JobFireTarget 投递——只有 subscriber/session
430
+ * 目标(不产生新 job)才放行。配 job 会产生新 job 高死循环风险,在此拒绝而非静默。
381
431
  */
382
- runNow(id: string): void;
432
+ private armIfEventDriven;
433
+ private disarmIfArmed;
434
+ private disarmAll;
383
435
  /**
384
- * RFC-229 M1: cancel the currently running job for a schedule task.
436
+ * owner 进程拒绝创建 session-local 任务时的诊断文案(RFC-357 D2 R3 的执行面)。
385
437
  *
386
- * Only cancels jobs in `running` or `needs_input` status — a job that has already
387
- * produced a diff (`ready`) is deliberately left untouched so the user can `/job apply`.
438
+ * **拒绝本身不放宽**——它防的是"任务出现在列表里却永远不触发"的静默失效:非 owner
439
+ * 进程不跑 ticker,任务又 durable:false 不落盘,owner 进程 loadAllTasks 也看不见它。
440
+ * 本方法只解决"用户知道被拒了,但不知道该怎么办":补上**谁持有**(本地实现可从锁
441
+ * 文件解出 pid/host)与**多久能好**(RFC-435 D4 的非 owner acquire 重试会在锁释放后
442
+ * 自动接管,无需重启进程)。
388
443
  *
389
- * Does NOT delete the task definition or alter the cron schedule. The task will fire
390
- * again at its next cron-triggered time.
391
- *
392
- * Sets a cancel intent in `cancelRequested` so the in-flight `fire()` loop stops
393
- * retrying — it does NOT prematurely remove the task from `firing` (that belongs to
394
- * `fire().finally`), avoiding a race window where the task appears idle but `fire()` is
395
- * still running (RFC-229 R10).
444
+ * 三态退化:远程实现无本地 pid 概念 无主体文案;`network-error` 时不能说"另一终端
445
+ * 持有"(可能压根没连上服务端,让用户去找一个不存在的第二终端是更糟的误导)——这条
446
+ * 三态区分沿用 `handlers/schedule.ts:105-109` 既有 readOnlyHint 范式,不另造一套判据。
396
447
  */
397
- cancel(taskId: string): CancelResult;
448
+ private describeSessionOwnershipRefusal;
398
449
  /**
399
- * Heartbeat: evict expired recurring tasks, find & fire due tasks.
400
- * Reentry guard (MF-1) prevents overlapping ticks.
401
- */
402
- private tick;
403
- /**
404
- * Execute a task: start a job, monitor completion via registry events,
405
- * retry on failure up to maxRetries. Handles one-shot cleanup.
450
+ * session-local 任务创建时绑定的 sessionId。
451
+ * RFC-406 D6:从 SessionFireTarget handler 的 getSessionId() 端口读取(旧 SchedulerDeps.getSessionId
452
+ * 语义迁入此 handler)。add() 用它来设置 boundSessionId。
406
453
  *
407
- * review S4 residual risk (accepted): fire() runs independent of the ticker — if this
408
- * process is suspended mid-fire and loses ownership (S4 stops the ticker on the next
409
- * tick, but does not abort in-flight fire() calls), this in-flight call can still resolve
410
- * and write a stale lastFiredAt/nextFireAt back to the store after a successor process
411
- * has taken over. Harmless one-time overwrite (this process's ticker is already stopped,
412
- * so no repeated corruption) — needs 5-min suspend + in-flight fire + a same-window write
413
- * race with the new owner to manifest. Revisit if it proves to matter in practice.
454
+ * 找不到 SessionFireTarget(消费方未装配 session 投递)时回退空字符串——此时 add()
455
+ * wantsSession 分支不会命中(session 任务无 handler fire),boundSessionId 也无意义。
414
456
  */
457
+ private getSessionIdForTask;
458
+ }
459
+ //#endregion
460
+ //#region src/cron-parser.d.ts
461
+ /**
462
+ * Parse a 5-field cron expression (minute hour dom month dow) and return the next
463
+ * fire time >= from (inclusive). All times in local timezone.
464
+ *
465
+ * Throws on invalid expressions.
466
+ *
467
+ * Field syntax: wildcard (*), single value (5), step (e.g. \*\/15 or 5\/15),
468
+ * range (1-5), list (1,15,30), range with step (1-10\/2).
469
+ *
470
+ * Unsupported: L, W, ?, #, name aliases (MON, JAN).
471
+ *
472
+ * ## DoM∧DoW semantics
473
+ *
474
+ * If both day-of-month and day-of-week are constrained (not a bare *),
475
+ * a date matches if **EITHER** field matches (standard vixie-cron OR behaviour).
476
+ *
477
+ * ## Day-of-week mapping
478
+ *
479
+ * 0 = Sunday, 1–6 = Monday–Saturday, 7 = also Sunday.
480
+ */
481
+ declare function parseCron(expr: string, from?: Date): Date;
482
+ //#endregion
483
+ //#region src/remote-schedule-store.d.ts
484
+ interface RemoteScheduleStoreOptions {
415
485
  /**
416
- * RFC-327 D2:向订阅者投递一次到点通知。
417
- *
418
- * `occurrenceId` `{taskId, 本次计划触发时刻}` 派生——**同一次计划触发的重复投递
419
- * 得到同一个 id**,这正是幂等键的意义:service 重启后收到重投也能识别"这次我处理过"。
420
- * 若用随机 id,重复投递会被当成两次不同触发,幂等失效。
421
- *
422
- * 返回是否送达;未送达(服务未运行/宿主未接线)只记日志,不抛错、不重试。
486
+ * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸
487
+ * origin,该值本身是完整资源路径(对齐 `otto serve` 后端约定)。内部用
488
+ * `new URL(sessionUrl).origin` 提取协议+主机+端口,再拼接 `/api/storage/schedules`——
489
+ * `remote-fire-ownership.ts` 的同一提取逻辑保持一致,不能直接拿 sessionUrl 当前缀用。
423
490
  */
424
- private fireSubscriber;
491
+ sessionUrl: string;
492
+ getAuth: () => Promise<{
493
+ token: string;
494
+ }>;
495
+ fetch?: typeof globalThis.fetch;
496
+ timeoutMs?: number;
497
+ wsKey?: string | (() => string | undefined);
498
+ }
499
+ /**
500
+ * Create a ScheduleStore backed by the persistenced `/api/storage/schedules` namespace's
501
+ * generic storage route (`storage.ts`, `namespace='schedules'`) — distinct from the
502
+ * lease sub-route (`schedule-lease.ts`, `/api/schedules/...`) which handles fire-ownership
503
+ * arbitration only.
504
+ */
505
+ declare function createRemoteScheduleStore(options: RemoteScheduleStoreOptions): ScheduleStore;
506
+ //#endregion
507
+ //#region src/schedule-lease-migrations.d.ts
508
+ declare const SCHEDULE_LEASE_MIGRATIONS: Migration[];
509
+ //#endregion
510
+ //#region src/remote-fire-ownership.d.ts
511
+ interface RemoteFireOwnershipOptions {
425
512
  /**
426
- * RFC-357 D1/D2:把一个 `target==='session'` 任务的 `prompt` 注入其 `boundSessionId`
427
- * 对应的当前会话。不建 worktree、不派 AgentJob、不进 `runningJobsByTaskId`/重试循环
428
- * (`cancel()` 对 session-local 任务的行为见 `cancel()` 自身的 not_running 分支)。
429
- *
430
- * 返回是否注入成功;失败(无注入端口/会话身份不匹配/会话已销毁等——由端口内部判断并
431
- * 折叠为 boolean)只记日志,不抛错、不重试——同 `fireSubscriber` 的旁路信号纪律。
513
+ * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸
514
+ * origin。该值本身是完整资源路径(对齐 `otto serve` 后端约定,见 RFC-146 §1.3 两条远程
515
+ * 产品线对照表),本构造函数内部用 `new URL(sessionUrl).origin` 提取协议+主机+端口,
516
+ * 再拼接 `/api/schedules/...` lease 路径——不能直接拿 `sessionUrl` 当 baseUrl 使用。
432
517
  */
433
- private fireSession;
434
- private fire;
518
+ sessionUrl: string;
519
+ wsKey: string;
520
+ getAuth: () => Promise<{
521
+ token: string;
522
+ }>;
523
+ fetch?: typeof globalThis.fetch;
524
+ timeoutMs?: number;
525
+ now?: () => number;
526
+ }
527
+ /** Create a server-arbitrated FireOwnership backed by persistenced's lease endpoints. */
528
+ declare function createRemoteFireOwnership(options: RemoteFireOwnershipOptions): FireOwnership;
529
+ //#endregion
530
+ //#region src/fire-logger.d.ts
531
+ declare class FireLogger {
532
+ private config;
533
+ constructor(config: ScheduleConfig);
435
534
  /**
436
- * Subscribe to AgentJobRegistry onUpdate / onExit and resolve when the
437
- * job reaches a conclusion:
438
- *
439
- * 'success': ready (diff produced) or applied
440
- * 'failed': failed
441
- * 'timeout': maxDurationMs elapsed while still running (triggers cancel)
442
- * 'canceled': the job was canceled (deliberately, by user via cancel(), not a failure)
443
- *
444
- * ready is NOT terminal in the engine, but for the schedule it's a success
445
- * (the diff exists, the user can /job apply later).
535
+ * 写一行 fire log ${OTTO_HOME}/schedule-logs/<taskId>-<epoch>.log,然后清理超出
536
+ * maxFireLogsPerTask 的旧日志。best-effort——错误静默吞。
446
537
  *
447
- * needs_input extends the timeout — the agent is blocked on user input
448
- * and should not be killed while waiting.
538
+ * RFC-345:影子态零磁盘写入——fire log 是纯本机持久化产物,影子进程跳过。
449
539
  */
450
- private waitForCompletion;
540
+ write(taskId: string, status: string): void;
541
+ /** 保留最新 maxFireLogsPerTask 条日志文件。 */
542
+ private prune;
451
543
  /**
452
- * Apply a deterministic per-task jitter to spread cron fires. Recurring tasks use a
453
- * 30-min window (spreads periodic load); one-shot tasks use a 90s window (review S2 —
454
- * RFC-087 §D3 intends one-shot "in ~30s/~90s", not "delayed by up to 30 min"; a 30-min
455
- * window made "remind me in 30 minutes" arrive up to ~61 minutes later).
456
- * Uses djb2 hash of the task id for reproducibility across restarts.
544
+ * 读取最近的 fire log 条目。返回最新 maxEntries 条,每条 { timestamp, status }。
457
545
  */
458
- private applyJitter;
546
+ read(taskId: string, maxEntries?: number): Array<{
547
+ timestamp: string;
548
+ status: string;
549
+ }>;
550
+ }
551
+ //#endregion
552
+ //#region src/fire-targets/job-fire-target.d.ts
553
+ interface JobFireTargetDeps {
554
+ jobRegistry: AgentJobRegistry;
459
555
  /**
460
- * Persist a single task (fire-and-forget). Snapshot-per-id means we never
461
- * rewrite the whole collection and never delete-by-diff that would let one
462
- * process delete another process's schedules (A2).
556
+ * Dispatch an agent job. Mirrors AgentJobService.start() shape:
557
+ * synchronously returns a record with at least { id }.
463
558
  */
559
+ startJob: (input: {
560
+ title: string;
561
+ prompt: string;
562
+ sessionId: string;
563
+ origin: 'user' | 'main'; /** RFC-438:per-job 指定模型与思考档位(task.modelId/thinkingLevel 透传)。 */
564
+ model?: {
565
+ id: string;
566
+ thinkingLevel?: ThinkingLevel;
567
+ };
568
+ }) => {
569
+ id: string;
570
+ };
464
571
  /**
465
- * RFC-357 D2(独立评审 R2 命中的咽喉点方案):`target==='session'` 任务永不落盘——
466
- * 这是唯一的判断点,`add()`/`update()`/tick 推进/fire 成功/fire 失败-取消/`flushAll()`
467
- * 全部调用点共享此拦截,不逐点打补丁(否则新增调用点时会漏防,正是原设计的漏洞)。
572
+ * RFC-435 D1:绑定会话 id 端口。serve/daemon 无人值守装配可缺省——
573
+ * job-only 模式无会话,fire sessionId 用空串(无会话绑定,notify 不投递)。
468
574
  */
469
- private saveOne;
575
+ getSessionId?: () => string;
576
+ /** RFC-230:重试之间的固定延迟(ms)。默认 0。 */
577
+ retryBackoffMs?: number;
578
+ /** 测试可注入假 sleep 避免真实等待。 */
579
+ sleep?: (ms: number) => Promise<void>;
580
+ /** FireLogger 实例(写 fire 日志) */
581
+ fireLogger: FireLogger;
582
+ /** needs_input 硬上限倍数(对齐 ScheduleConfig.needsInputMaxExtensionFactor,默认 3)。 */
583
+ needsInputMaxExtensionFactor?: number;
584
+ }
585
+ declare class JobFireTarget implements FireTarget {
586
+ private deps;
587
+ readonly type = "job";
588
+ private readonly runningJobsByTaskId;
589
+ private readonly cancelRequested;
590
+ private _nextFireToken;
591
+ constructor(deps: JobFireTargetDeps);
592
+ canHandle(task: ScheduledTask): boolean;
593
+ fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult>;
594
+ /**
595
+ * 设置 cancel 意图——fire() 循环在下次 attempt 前检查并停止。
596
+ * 同时调用 jobRegistry.cancel() 终止当前正在运行的 job。
597
+ * 返回 CancelResult(与 SchedulerService.cancel() 的返回类型对齐)。
598
+ */
599
+ requestCancel(taskId: string): CancelResult;
600
+ /** 清理 fire 完成后的记账状态。由 FireExecutor 在 fire().finally 中调用。 */
601
+ cleanup(taskId: string): void;
602
+ /** 当前有正在运行的 job 的 task id 集合。 */
603
+ getRunningTaskIds(): Set<string>;
604
+ private waitForCompletion;
605
+ }
606
+ //#endregion
607
+ //#region src/fire-targets/session-fire-target.d.ts
608
+ interface SessionFireTargetDeps {
470
609
  /**
471
- * Delete a single task snapshot (awaited — must confirm removal to prevent zombies on
472
- * store reload). RFC-357 D2:session-local 任务从不落盘,删除时直接视为成功(无需真的
473
- * 调用 store——它本就不在 store 里),避免对不存在的记录发起一次多余的 IO。
610
+ * prompt 注入 boundSessionId 对应的当前会话。
611
+ * 返回 true=已成功注入;false=失效(会话身份不匹配/已销毁/archived/paused 等)。
474
612
  */
475
- private deleteOne;
613
+ followUp: (task: {
614
+ prompt: string;
615
+ boundSessionId: string;
616
+ name: string;
617
+ origin: 'user' | 'model';
618
+ }) => boolean;
476
619
  /**
477
- * Flush every in-memory task to the store (used on stop; bounded, no diff-delete).
478
- * RFC-357 D2(独立评审 R2):跳过 `target==='session'` 任务——否则进程正常退出时会把
479
- * 内存中的 session-local 任务写进 store,下次 `start()` 从 store 加载后在**新进程的新
480
- * sessionRef** 上"复活",直接击穿"不落盘"这条防线(本 RFC 最初设计遗漏的关键路径)。
620
+ * RFC-406 D6:session-local 任务创建时绑定的当前 sessionId 端口。
621
+ * scheduler.add() SchedulerService.getSessionIdForTask() 读取此值填充 boundSessionId。
622
+ * (旧 SchedulerDeps.getSessionId 的语义迁入此 handler。)
481
623
  */
482
- private flushAll;
624
+ getSessionId: () => string;
625
+ /** FireLogger 实例(写 fire 日志) */
626
+ fireLogger: FireLogger;
627
+ }
628
+ declare class SessionFireTarget implements FireTarget {
629
+ private deps;
630
+ readonly type = "session";
631
+ constructor(deps: SessionFireTargetDeps);
632
+ canHandle(task: ScheduledTask): boolean;
633
+ /** RFC-406 D6:暴露当前 sessionId 供 scheduler.add() 绑定 boundSessionId。 */
634
+ getSessionId(): string;
635
+ fire(task: ScheduledTask, _ctx: FireContext): Promise<FireResult>;
636
+ }
637
+ //#endregion
638
+ //#region src/fire-targets/subscriber-fire-target.d.ts
639
+ interface SubscriberFireTargetDeps {
483
640
  /**
484
- * Write a one-line fire log to ${OTTO_HOME}/schedule-logs/<taskId>-<epoch>.log, then prune
485
- * older logs for this task beyond MAX_FIRE_LOGS_PER_TASK (review S7: a `* * * * *` task
486
- * fires 1440 times/day with no prior retention — the directory grew unbounded). Best-effort
487
- * throughout — swallows errors silently (logging is non-critical).
641
+ * 向订阅者投递到点通知的窄端口。返回是否真的送达——false 表示服务未运行/已停机。
642
+ * 端口刻意只接受纯数据标识:handler 不知道插件服务的存在形式,也不持有其句柄。
488
643
  */
489
- private writeFireLog;
490
- /** Keep only the newest MAX_FIRE_LOGS_PER_TASK log files for a given taskId. */
491
- private pruneFireLogs;
644
+ notify: (subscriber: ScheduleSubscriber, tick: {
645
+ scheduleId: string;
646
+ occurrenceId: string;
647
+ occurredAt: number;
648
+ }) => boolean;
649
+ /** FireLogger 实例(写 fire 日志) */
650
+ fireLogger: FireLogger;
651
+ }
652
+ declare class SubscriberFireTarget implements FireTarget {
653
+ private deps;
654
+ readonly type = "plugin-service";
655
+ constructor(deps: SubscriberFireTargetDeps);
656
+ canHandle(task: ScheduledTask): boolean;
657
+ fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult>;
658
+ }
659
+ //#endregion
660
+ //#region src/triggers/in-process-event-trigger.d.ts
661
+ interface InProcessEventTriggerDeps {
662
+ jobRegistry: AgentJobRegistry;
663
+ now?: () => number;
664
+ logger?: {
665
+ error: (msg: string | Error, ...args: unknown[]) => void;
666
+ };
667
+ /** 闸门B:debounce 窗口(ms)。缺省 60s。 */
668
+ debounceWindowMs?: number;
669
+ /** 闸门B:窗口内最大触发次数。缺省 5。 */
670
+ maxFiresPerWindow?: number;
671
+ }
672
+ declare class InProcessEventTrigger implements EventDrivenTrigger {
673
+ private deps;
674
+ readonly type = "in-process-event";
675
+ /** 闸门B:每任务的触发时刻队列(滑动窗口)。 */
676
+ private readonly fireTimestamps;
677
+ private readonly now;
678
+ private readonly debounceWindowMs;
679
+ private readonly maxFiresPerWindow;
680
+ constructor(deps: InProcessEventTriggerDeps);
681
+ canHandle(task: ScheduledTask): boolean;
682
+ arm(task: ScheduledTask, fire: (task: ScheduledTask) => void): () => void;
683
+ private matches;
684
+ /** 闸门B:滑动窗口频率检查。返回 true=放行,false=超限拦截。 */
685
+ private passesDebounce;
492
686
  }
493
687
  //#endregion
494
688
  //#region src/schedule-capability.d.ts
@@ -506,6 +700,19 @@ interface ScheduleCapability {
506
700
  * the model-facing guidance on when to pick which.
507
701
  */
508
702
  target?: 'job' | 'session';
703
+ /**
704
+ * RFC-423:进程内事件触发。存在时任务由事件驱动(如 job diff applied)而非 cron。
705
+ * 必须配 target:'session'(或 subscriber)——事件触发不允许 job 投递(死循环闸门)。
706
+ */
707
+ eventTrigger?: {
708
+ source: string;
709
+ event: string;
710
+ filter?: {
711
+ sessionId?: string;
712
+ };
713
+ }; /** RFC-438:到点执行时使用的模型 id(如 "deepseek/deepseek-v4-flash")。缺省 = 当前会话模型。 */
714
+ modelId?: string; /** RFC-438:思考档位(配 modelId 使用)。 */
715
+ thinkingLevel?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
509
716
  }) => {
510
717
  taskId: string;
511
718
  nextFireAt: number;
@@ -531,7 +738,14 @@ interface ScheduleCapability {
531
738
  reason?: string;
532
739
  };
533
740
  }
534
- declare function createScheduleCapability(getScheduler: () => SchedulerService | undefined): ScheduleCapability;
741
+ interface ScheduleCapabilityDeps {
742
+ /**
743
+ * RFC-438 D3b:create 时校验模型 id。返回错误信息或 undefined(合法)。
744
+ * 缺省 = 不校验(运行时 fail-closed 兜底——fire 时 resolveJobModel 抛错)。
745
+ */
746
+ validateModelId?: (id: string) => string | undefined;
747
+ }
748
+ declare function createScheduleCapability(getScheduler: () => SchedulerService | undefined, deps?: ScheduleCapabilityDeps): ScheduleCapability;
535
749
  //#endregion
536
- export { type CancelResult, type FireOwnership, type RemoteFireOwnershipOptions, type RemoteScheduleStoreOptions, SCHEDULE_LEASE_MIGRATIONS, type ScheduleCapability, ScheduleRegistry, type ScheduleStore, type ScheduleSubscriber, type ScheduledTask, type ScheduledTaskInput, type SchedulerDeps, SchedulerService, createLocalFireOwnership, createRemoteFireOwnership, createRemoteScheduleStore, createScheduleCapability, createScheduleStore, parseCron };
750
+ export { DEFAULT_SCHEDULE_CONFIG, type EventDrivenTrigger, type EventTriggerSpec, FireLogger, type FireOwnership, type FireTarget, InProcessEventTrigger, JobFireTarget, SCHEDULE_LEASE_MIGRATIONS, ScheduleRegistry, type ScheduleStore, type ScheduleSubscriber, type ScheduledTask, type ScheduledTaskInput, type SchedulerDeps, SchedulerService, SessionFireTarget, SubscriberFireTarget, type TerminationReason, createLocalFireOwnership, createRemoteFireOwnership, createRemoteScheduleStore, createScheduleCapability, createScheduleStore, parseCron };
537
751
  //# sourceMappingURL=index.d.ts.map