@zhushanwen/pi-scheduler 0.9.2 → 0.9.3

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/src/runtime.ts CHANGED
@@ -16,15 +16,28 @@ const logger = getLogger('scheduler')
16
16
 
17
17
  const MAX_TASKS = 50
18
18
  const RATE_LIMIT_PER_MINUTE = 6
19
- // 导出:ack 写盘自检复用同一节奏常量(禁新造魔数),值 = tick 间隔。
19
+ // tick 间隔(30s)。导出供测试 import 复用(fake timers 推进量与生产节奏同源——禁
20
+ // 测试本地重造 30_000 魔数,改值即测试锚同步漂移);ack 写盘自检为事件驱动
21
+ //(maybeStartAck 由任务创建回调触发,见 index.ts 创建汇聚点;message_start /
22
+ // turn_end 挂点只承载覆写武装与安全网注销),不引用本常量。
20
23
  export const TICK_INTERVAL_MS = 30_000
21
24
  const DEFAULT_EXPIRY_DAYS = 7
22
25
  const DEFAULT_EXPIRY_MS = DEFAULT_EXPIRY_DAYS * MS_PER_DAY // 7 days
23
- // U4 dispatch 模型切换(设计 D3 修订版):
24
- // - dispatch 注入消息的 customType 标记前缀(dispatchTaskInner 的 sendMessage 与
25
- // handleMessageStart 的归属匹配共用单点,字面漂移会静默断开事件恢复链路)
26
+ // U4 dispatch 模型切换(设计 D3 修订版 + D1 归属简化):
27
+ // - dispatch 注入消息的 customType 标记前缀(sendMessage 的消息标记,供运维从 session 流
28
+ // 识别 scheduler dispatch 注入;与 ack 触发器前缀 pi-scheduler-ack: 避让)
26
29
  const DISPATCH_CUSTOM_TYPE_PREFIX = 'pi-scheduler:'
27
- // - 在途标记强制开放的 tick 计数(D3 在途标记生命周期:超过 2 个 tick 未关闭即视为事件丢失)
30
+ // - 在途标记强制开放的 tick 计数:超过本计数的 tick 数未关闭即视为恢复事件丢失
31
+ // [时间平抑红线登记](30s tick 周期对账 = 用时间换一致的兜底):
32
+ // 补偿根因:agent_settled 恢复事件丢失(pi 事件通道无送达保证)时,未决记录无事件通道
33
+ // 复位,会话停留在任务模型上不被切回。
34
+ // 量级/形态:TICK_INTERVAL_MS=30s 周期 tick 对账;in-flight 标记超本计数(2 tick ≈ 60s,
35
+ // 强制开放的最坏恢复延迟 ≈ 3 tick ≈ 90s)未关闭即视为事件丢失、强制开放转移。
36
+ // 恢复路径:reconcileModelSwitch 强制开放 → idle 即 restoreExpectedModel 切回期望模型 /
37
+ // 非 idle 转 awaiting-restore(tick 重入 idle 兑现);恢复失败落 warn 日志(含
38
+ // "manual switch back required" 恢复动作)。
39
+ // 重审触发(退役条件):恢复事件闭环已由 agent_settled 单点达成(D1 归属简化);待批次 5
40
+ // W2 通道可观测(事件丢失可报警)落地后,本对账强制开放分支删除(设计 §3.3 D-B4-4)。
28
41
  const MODEL_SWITCH_RECONCILE_TICKS = 2
29
42
 
30
43
  /**
@@ -47,14 +60,15 @@ export interface SchedulerModelOps {
47
60
  }
48
61
 
49
62
  /**
50
- * 未决模型切换记录(设计 D3 状态机三要素之一,纯内存态)。模型语义异常不进持久化词表
63
+ * 未决模型切换记录(模型状态机,纯内存态)。模型语义异常不进持久化词表
51
64
  * (replay 守卫对 advance.status 硬校验 'success',旁路字段会炸重放推进)。
52
65
  *
53
- * phase 生命周期(在途标记三分):
54
- * - 'in-flight':等待 dispatched turn 的 turn_end 恢复;ticksOpen 计数超过
55
- * MODEL_SWITCH_RECONCILE_TICKS → 事件丢失,对账强制开放
56
- * - 'awaiting-restore':非 idle 推迟 / 强制开放转推迟;比对决策已在窗口内锁定,
57
- * 执行不受窗口过期限制——tick 重入 idle 即兑现(不设墙钟上界)
66
+ * phase 生命周期(D1 归属简化后二态;恢复事件 = agent_settled 单点):
67
+ * - 'in-flight':dispatch 受理后等待 run 落定(agent_settled)兑现恢复;ticksOpen 计数超过
68
+ * MODEL_SWITCH_RECONCILE_TICKS → 恢复事件丢失,tick 对账强制开放
69
+ * - 'awaiting-restore':settled 到达时非 idle(与用户新 run 交错)/ 对账强制开放时非 idle
70
+ * 的推迟态;比对决策已在窗口内锁定,执行不受窗口过期限制——tick 重入 idle 即兑现
71
+ * (不设墙钟上界)
58
72
  * - 恢复成功 / restore-failed(setModel(原) false)→ 记录清除(终态只留日志)
59
73
  */
60
74
  interface PendingModelSwitch {
@@ -66,18 +80,6 @@ interface PendingModelSwitch {
66
80
  ticksOpen: number
67
81
  }
68
82
 
69
- /**
70
- * message_start 的 custom message 形状守卫(unknown 收窄,无断言):
71
- * role:'custom' 且 customType 以 pi-scheduler: 开头(dispatch 注入标记,P-MODEL-④)。
72
- */
73
- function matchesDispatchCustomType(message: unknown): boolean {
74
- if (typeof message !== 'object' || message === null) return false
75
- if (!('role' in message) || !('customType' in message)) return false
76
- if (message.role !== 'custom') return false
77
- return (
78
- typeof message.customType === 'string' && message.customType.startsWith(DISPATCH_CUSTOM_TYPE_PREFIX)
79
- )
80
- }
81
83
  // HISTORY_LIMIT 单点在 types.ts(ext-simplify-08 L5)——与 replay.ts 的 advance 折叠共用
82
84
  // STALE_CTX_MARKER(文案兜底分诊词)已迁移到 ext-guards 共享守卫(guardStaleCtx 内部
83
85
  // 引用,本文件不再直接持有)。语义:G1 模块级代际检测(isCtxStale)为主判;文案子串
@@ -107,11 +109,6 @@ export class SchedulerRuntime {
107
109
  // 结构性不可能(完成顺序不定 → turn 静默跑错模型的交错被消除)。链条引用恒吞错
108
110
  // 自愈(见 serializeModelOp),前序 op 失败不阻断后继。
109
111
  private modelOpChain: Promise<void> = Promise.resolve()
110
- // 归属状态机 run 窗口(P-MODEL-③④:turnIndex per-run 归零,禁止裸 turnIndex 跨 run 当 id):
111
- // agent_start 起算(重置 turn 序态)、agent_end / agent_settled 封口
112
- private runWindowActive = false
113
- private currentTurnIndex = -1
114
- private dispatchedTurnIndex: number | null = null
115
112
 
116
113
  /**
117
114
  * 依赖反转构造:backend 承担 appendEntry/pi.sendMessage/时间源,runtime 只持有内存态。
@@ -128,6 +125,13 @@ export class SchedulerRuntime {
128
125
  this.backend = backend
129
126
  this.isCtxStale = isCtxStale
130
127
  this.modelOps = modelOps
128
+ // U6c:session_tree(树回退)重折叠接线——生产 backend 在 pi.on('session_tree')
129
+ // 时按活跃路径重折叠任务集并回调;落点 = 本 tasks Map(loadTasks 换集)。
130
+ // 纯重建:不 dispatch、不 append op、不动 tickTimer 启停(常驻循环照常消费
131
+ // 新任务集,被撤子树任务不在集内即到点不触发)也不动 pendingModelSwitch
132
+ // (撤回编排前置 = 会话空闲,无在途模型切换记录)。测试替身不实现
133
+ // onSessionTree 时 ?. 订阅为 no-op,既有单测路径不变。
134
+ backend.onSessionTree?.((tasks) => this.loadTasks(tasks))
131
135
  }
132
136
 
133
137
  // ── 任务 CRUD ──
@@ -305,7 +309,7 @@ export class SchedulerRuntime {
305
309
  // 「B dispatch 先切 Y → 对账持 A 旧记录把 Y 静默切回」的交错(Y 从未生效、全序列
306
310
  // 无报错,G4 被绕过)。恢复执行时 await 完成,保证恢复 setModel 完成后 dispatch 循环
307
311
  // 才开 setModel;更广的「任意两个 setModel 不并发」由模型 op 串行队列(modelOpChain)
308
- // 结构保证——含 turn_end 恢复 fire-and-forget 挂点,dispatch 模型分支排在其后。
312
+ // 结构保证——含 agent_settled 恢复 fire-and-forget 挂点,dispatch 模型分支排在其后。
309
313
  // 无未决记录的绝大多数 tick 同步返回、不引入 microtask 断点——dispatch 的同步段
310
314
  // 契约(in-flight 守卫在 tickScheduler() 未 await 时同步可见)保持不变。
311
315
  const reconciling = this.reconcileModelSwitch()
@@ -421,7 +425,7 @@ export class SchedulerRuntime {
421
425
  skipped = true
422
426
  } else {
423
427
  // 模型 op 串行段(MF-2 收口,互斥窗口全程覆盖「setModel 起步 → 记录创建」):
424
- // 前序模型 op(turn_end 恢复 / 前序 dispatch 切换 / 错误路径恢复)可能仍在途——
428
+ // 前序模型 op(agent_settled 恢复 / 前序 dispatch 切换 / 错误路径恢复)可能仍在途——
425
429
  // 排队等待期间未决记录与当前模型均可能变化,禁止按排队前快照盲切(旧快照的
426
430
  // expectedModelRef 会把恢复锚点记错),串行段内重新校验后再切换。后来的需切模型
427
431
  // 任务同样排队、出队时重新校验命中互斥 skip——双记录叠写 / 两个 setModel 并发
@@ -448,7 +452,7 @@ export class SchedulerRuntime {
448
452
  const switched = await modelOps.setModelByRef(targetModelRef)
449
453
  if (switched) {
450
454
  // setModel 受理 → 记未决切换记录(会话期望模型 + 在途标记,内存态);
451
- // sendMessage 受理后等事件恢复(message_start 归属 → turn_end + isIdle 复核)。
455
+ // sendMessage 受理后等 run 落定(agent_settled + isIdle 复核)兑现恢复。
452
456
  // 记录创建收在同一串行段内:互斥窗口对后继模型任务即时可见,无「已切未记」
453
457
  // 的放行缺口。
454
458
  this.pendingModelSwitch = {
@@ -458,7 +462,6 @@ export class SchedulerRuntime {
458
462
  phase: 'in-flight',
459
463
  ticksOpen: 0,
460
464
  }
461
- this.dispatchedTurnIndex = null
462
465
  } else {
463
466
  // setModel false(无可用 API key 等)→ 按「恢复失败」同族处理:日志 + 放弃本次
464
467
  // 切换,任务照常 dispatch(模型字段不阻塞核心调度)
@@ -489,7 +492,6 @@ export class SchedulerRuntime {
489
492
  const ps = this.pendingModelSwitch
490
493
  if (ps && modelOps) {
491
494
  this.pendingModelSwitch = null
492
- this.dispatchedTurnIndex = null
493
495
  // 恢复 setModel 入模型 op 串行队列(与其他 setModel 发起点互斥,不并发);排队后
494
496
  // 复核「当前 == 期望」:等待期间已切回则不动作
495
497
  await this.serializeModelOp(async () => {
@@ -552,15 +554,15 @@ export class SchedulerRuntime {
552
554
  return this.dispatchTimestamps.length < RATE_LIMIT_PER_MINUTE
553
555
  }
554
556
 
555
- // ── 模型切换:对账兜底与归属状态机(U4,设计 D3 修订版)──
557
+ // ── 模型切换:agent_settled 恢复与 tick 对账兜底(U4,D3 修订版 + D1 归属简化)──
556
558
 
557
559
  /**
558
560
  * 未决模型切换记录的对账探测(tickScheduler 开头调用,严格先于 dispatch 循环)。
559
561
  * 同步推进状态机,仅在需要执行恢复时返回 Promise(调用方 await——顺序约束)。
560
562
  * 对账守卫前置:仅当存在未决记录才比对恢复——无记录时「模型 ≠ 期望」是用户自主行为,
561
563
  * 不动作(否则用户在任务间隙手动切模型会被静默回滚)。
562
- * - in-flight:ticksOpen 计数超 MODEL_SWITCH_RECONCILE_TICKS → 恢复回调永不执行(事件
563
- * 丢失),强制开放:idle 即恢复(返回恢复 Promise);非 idle 转 awaiting-restore
564
+ * - in-flight:ticksOpen 计数超 MODEL_SWITCH_RECONCILE_TICKS → agent_settled 恢复事件
565
+ * 丢失,强制开放:idle 即恢复(返回恢复 Promise);非 idle 转 awaiting-restore
564
566
  * (P-MODEL-② 证实 setModel 不影响 in-flight turn,但保守口径维持非 idle 推迟。
565
567
  * pi 实装锚点(0.84.4):dist/core/agent-session.js:304 turn 模型在准备时一次性
566
568
  * 快照,setModel 只写 state 不回写已在途 turn 的已快照模型)。
@@ -638,8 +640,8 @@ export class SchedulerRuntime {
638
640
  * 执行恢复(切回会话期望模型)并关闭未决记录(在途标记生命周期收口):
639
641
  * 当前模型已等于期望(用户已手动切回)→ 清记录不动作;setModel(原) false →
640
642
  * restore-failed 终态(日志含恢复动作:会话停留在任务模型,需手动切回)+ 清记录。
641
- * 记录先关后恢复——防 await setModel 期间 turn_end 与 tick 对账并发重入。
642
- * 恢复 setModel 本体经模型 op 串行队列(serializeModelOp,MF-2):turn_end 挂点的
643
+ * 记录先关后恢复——防 await setModel 期间 agent_settled 与 tick 对账并发重入。
644
+ * 恢复 setModel 本体经模型 op 串行队列(serializeModelOp,MF-2):agent_settled 挂点的
643
645
  * fire-and-forget 调用由此变为安全——在途恢复持队,后续任何 setModel 发起点
644
646
  * (dispatch 切换 / 对账恢复)排队等待,两个 setModel 并发结构性不可能(完成顺序
645
647
  * 不定 → turn 静默跑错模型的交错被消除)。「当前 == 期望」复核移入串行段:排队
@@ -650,7 +652,6 @@ export class SchedulerRuntime {
650
652
  const ops = this.modelOps
651
653
  if (!ps || !ops) return Promise.resolve()
652
654
  this.pendingModelSwitch = null
653
- this.dispatchedTurnIndex = null
654
655
  return this.serializeModelOp(async () => {
655
656
  if (ops.getCurrentModelRef() === ps.expectedModelRef) return
656
657
  const restored = await ops.setModelByRef(ps.expectedModelRef)
@@ -665,87 +666,35 @@ export class SchedulerRuntime {
665
666
  })
666
667
  }
667
668
 
668
- // 归属状态机事件入口(index.ts 的 pi.on 转发;方法内不触碰捕获的 pi/ctx——恢复动作
669
- // 经构造注入的 modelOps,stale 代际由 isCtxStale 入口检查拦下:清状态机、不执行恢复)。
670
-
671
- /** agent run 开始:重置 run 窗口与 turn 序态(P-MODEL-③:turnIndex per-run 归零) */
672
- handleAgentStart(): void {
673
- if (this.isCtxStale?.()) return
674
- this.runWindowActive = true
675
- this.currentTurnIndex = -1
676
- }
677
-
678
- /** turn 开始:监听器维护「当前 turnIndex」(message_start 序态关联的基准,P-MODEL-④)。
679
- * turnIndex 缺省(pi 契约恒有;测试仿真可无参调 handler)→ 忽略本次序态更新。 */
680
- handleTurnStart(turnIndex: number | undefined): void {
681
- if (this.isCtxStale?.()) return
682
- if (typeof turnIndex !== 'number') return
683
- this.currentTurnIndex = turnIndex
684
- }
669
+ // 模型恢复事件入口(index.ts 的 pi.on('agent_settled') 转发;方法内不触碰捕获的 pi/ctx
670
+ // ——恢复动作经构造注入的 modelOps,stale 代际由 isCtxStale 入口检查拦下:清状态机、
671
+ // 不执行恢复)。
685
672
 
686
673
  /**
687
- * message_start 归属匹配(P-MODEL-④ 实测序态 turn_start(n) → message_start(custom) →
688
- * … → turn_end(n)):customType 前缀命中 → 记 dispatchedTurnIndex = 当前 turnIndex。
689
- * 仅在未决记录在场且 run 窗口活跃时归属——避免陈旧索引在记录关闭后意外匹配后续 turn,
690
- * 以及裸 turnIndex 跨 run 当 id。
674
+ * agent_settled:模型恢复的唯一事件通道(D1 归属简化,审计裁决「agent_settled + isIdle
675
+ * 复核拆 turnIndex 状态机」——原 turnIndex 归属:4 状态字段 + 6 事件挂点 + customType
676
+ * 前缀匹配,绕路自造对 2-tick 对账的依赖)。settled = run 完全落定(无 retry/compaction/
677
+ * queued continuation;pi 实装 agent-session.js _emitAgentSettled 先置 _isAgentRunActive
678
+ * = false 再 emit),dispatched turn 必已结束,无需 turnIndex 归属识别——isIdle 复核:
679
+ * - idle → 兑现恢复(in-flight / awaiting-restore 均结算;恢复延迟从「最长 1 tick(30s)」
680
+ * 收敛到事件即时)
681
+ * - 非 idle(settled 与用户新 run 交错)→ 转 awaiting-restore 推迟(不能切:会把新 run
682
+ * 的模型换掉,P-MODEL-② 同族形态),tick 重入 idle 兑现
683
+ * 不挂 agent_end 恢复:end 后仍可能有自动续跑 turn,此时切回会把续跑 turn 的模型换掉
684
+ * (P-MODEL-② 同族形态)——恢复只认 settled。agent_settled 丢失(事件通道无送达保证)
685
+ * → 30s tick 对账强制开放兜底(见 MODEL_SWITCH_RECONCILE_TICKS 红线登记)。
691
686
  */
692
- handleMessageStart(message: unknown): void {
693
- if (this.isCtxStale?.()) return
694
- if (!this.pendingModelSwitch || !this.runWindowActive) return
695
- if (!matchesDispatchCustomType(message)) return
696
- this.dispatchedTurnIndex = this.currentTurnIndex
697
- }
698
-
699
- /**
700
- * turn_end 恢复挂点(设计 D3 修订版):turnIndex === dispatchedTurnIndex → isIdle 复核——
701
- * idle 即恢复;非 idle(用户长 run 的后续 turn 在途)推迟(phase 转 awaiting-restore,
702
- * tick 重入 idle 即兑现)。裸 turn_end 挂点会在用户 run 未结束时提前恢复、把该 run 后续
703
- * turn 切回原模型,禁止(P-MODEL-② 实测形态)。turnIndex 缺省(同 handleTurnStart)不匹配。
704
- */
705
- handleTurnEnd(turnIndex: number | undefined): void {
687
+ handleRunSettled(): void {
706
688
  if (this.isCtxStale?.()) return
707
689
  const ps = this.pendingModelSwitch
708
- if (
709
- !ps ||
710
- typeof turnIndex !== 'number' ||
711
- this.dispatchedTurnIndex === null ||
712
- turnIndex !== this.dispatchedTurnIndex
713
- ) {
714
- return
715
- }
716
- if (this.modelOps?.isIdle()) {
717
- void this.restoreExpectedModel('turn-end')
690
+ if (!ps || !this.modelOps) return
691
+ if (this.modelOps.isIdle()) {
692
+ void this.restoreExpectedModel('agent-settled')
718
693
  } else {
719
694
  ps.phase = 'awaiting-restore'
720
695
  }
721
696
  }
722
697
 
723
- /** agent_end / agent_settled 封口 run 窗口(窗口外 turnIndex 不作归属 id) */
724
- handleRunClosed(): void {
725
- if (this.isCtxStale?.()) return
726
- this.runWindowActive = false
727
- this.currentTurnIndex = -1
728
- this.dispatchedTurnIndex = null
729
- }
730
-
731
- /**
732
- * agent_settled:封口 run 窗口(同 handleRunClosed)+ awaiting-restore 的即时兑现。
733
- * settled = run 完全落定(无 retry/compaction/queued continuation;pi 实装
734
- * agent-session.js _emitAgentSettled 先置 _isAgentRunActive=false 再 emit),正是
735
- * awaiting-restore 推迟恢复所等的「真空闲」时刻——挂上后恢复延迟从「最长 1 tick
736
- * (30s)」收敛到事件即时。只挂 settled 不挂 agent_end:end 后仍可能有自动续跑
737
- * turn,此时切回会把续跑 turn 的模型换掉(P-MODEL-② 同族形态)。
738
- * 不动 in-flight(turn_end 丢失的归属异常归 tick 对账——2 tick 强制开放既有兜底);
739
- * settled 与用户新 run 交错(isIdle false)不动作留给 tick,兜底语义不变。
740
- */
741
- handleRunSettled(): void {
742
- if (this.isCtxStale?.()) return
743
- this.handleRunClosed()
744
- const ps = this.pendingModelSwitch
745
- if (ps?.phase !== 'awaiting-restore' || !this.modelOps?.isIdle()) return
746
- void this.restoreExpectedModel('agent-settled')
747
- }
748
-
749
698
  /**
750
699
  * ERR-2 fallback(ext-simplify-17 B3 抽取):cron 表达式失效 → 停用任务并记录失败原因
751
700
  * (toggle enable 重算与 dispatch 成功推进两处共用)。禁止 `?? now()` 类 fallback
@@ -760,7 +709,10 @@ export class SchedulerRuntime {
760
709
 
761
710
  // ── 装配与回调 ──
762
711
 
763
- /** 装配点注入初始任务数组(读盘/重放由 backend 完成,runtime 只持有内存态)。 */
712
+ /**
713
+ * 任务集换装入口(装配点 session_start 注入初始数组 + U6c session_tree 重折叠共用;
714
+ * 读盘/重放由 backend 完成,runtime 只持有内存态)。
715
+ */
764
716
  loadTasks(tasks: ScheduledTask[]): void {
765
717
  this.tasks = new Map(tasks.map(t => [t.id, t]))
766
718
  }
package/src/types.ts CHANGED
@@ -69,7 +69,7 @@ export interface AddOptions {
69
69
  // 与单测夹具共同消费。
70
70
 
71
71
  /**
72
- * ack 触发器 custom message 的 customType 前缀:避让 dispatch 归属前缀 `pi-scheduler:`,
72
+ * ack 触发器 custom message 的 customType 前缀:避让 dispatch 消息标记前缀 `pi-scheduler:`,
73
73
  * 使 `message_start` 武装判别(startsWith)不会把 dispatch / 任务消息误识别为触发器。
74
74
  */
75
75
  export const ACK_CUSTOM_TYPE_PREFIX = 'pi-scheduler-ack:'