@zhushanwen/pi-scheduler 0.9.2 → 0.10.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/src/runtime.ts CHANGED
@@ -16,16 +16,21 @@ 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 归属简化 + ADR-0122 事实查询翻转):
27
+ // - dispatch 注入消息的 customType 标记前缀(sendMessage 的消息标记,供运维从 session 流
28
+ // 识别 scheduler dispatch 注入;与 ack 触发器前缀 pi-scheduler-ack: 避让)
29
+ // - 恢复判定锚点 = isIdle() 同步事实查询(判定层级最高):agent_settled 确定性事件是
30
+ // 提前触发的加速器(事件到达即查询并兑现);每 tick 开头对未决记录做一次同步事实
31
+ // 查询兜正确性(事件丢失时由查询收敛——拉保证正确、事件只优化延迟)。原 2-tick
32
+ // 计数对账(时间窗猜测事件丢失)已按 ADR-0122 删除。
26
33
  const DISPATCH_CUSTOM_TYPE_PREFIX = 'pi-scheduler:'
27
- // - 在途标记强制开放的 tick 计数(D3 在途标记生命周期:超过 2 个 tick 未关闭即视为事件丢失)
28
- const MODEL_SWITCH_RECONCILE_TICKS = 2
29
34
 
30
35
  /**
31
36
  * 模型控制面(U4 dispatch 模型切换,依赖反转):runtime 只见 'provider/id' 字符串 ref
@@ -47,37 +52,21 @@ export interface SchedulerModelOps {
47
52
  }
48
53
 
49
54
  /**
50
- * 未决模型切换记录(设计 D3 状态机三要素之一,纯内存态)。模型语义异常不进持久化词表
55
+ * 未决模型切换记录(纯内存态)。模型语义异常不进持久化词表
51
56
  * (replay 守卫对 advance.status 硬校验 'success',旁路字段会炸重放推进)。
52
57
  *
53
- * phase 生命周期(在途标记三分):
54
- * - 'in-flight':等待 dispatched turn 的 turn_end 恢复;ticksOpen 计数超过
55
- * MODEL_SWITCH_RECONCILE_TICKS → 事件丢失,对账强制开放
56
- * - 'awaiting-restore':非 idle 推迟 / 强制开放转推迟;比对决策已在窗口内锁定,
57
- * 执行不受窗口过期限制——tick 重入 idle 即兑现(不设墙钟上界)
58
- * - 恢复成功 / restore-failed(setModel(原) false)→ 记录清除(终态只留日志)
58
+ * 生命周期(ADR-0122 事实查询翻转后):dispatch 受理 → 记录创建(in-flight 等价语义:
59
+ * 等 run 落定);恢复兑现锚点 = isIdle() 同步事实查询(agent_settled 事件提前触发 +
60
+ * tick 每轮查询兜正确性)→ 恢复成功 / restore-failed(setModel(原) false)→ 记录清除
61
+ * (终态只留日志)。记录存在 + idle 即恢复,无 phase 区分、无时间窗计数。
59
62
  */
60
63
  interface PendingModelSwitch {
61
64
  taskId: string
62
65
  /** 会话期望模型(dispatch 切换前抓取的原模型 'provider/id') */
63
66
  expectedModelRef: string
64
67
  targetModelRef: string
65
- phase: 'in-flight' | 'awaiting-restore'
66
- ticksOpen: number
67
68
  }
68
69
 
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
70
  // HISTORY_LIMIT 单点在 types.ts(ext-simplify-08 L5)——与 replay.ts 的 advance 折叠共用
82
71
  // STALE_CTX_MARKER(文案兜底分诊词)已迁移到 ext-guards 共享守卫(guardStaleCtx 内部
83
72
  // 引用,本文件不再直接持有)。语义:G1 模块级代际检测(isCtxStale)为主判;文案子串
@@ -107,11 +96,6 @@ export class SchedulerRuntime {
107
96
  // 结构性不可能(完成顺序不定 → turn 静默跑错模型的交错被消除)。链条引用恒吞错
108
97
  // 自愈(见 serializeModelOp),前序 op 失败不阻断后继。
109
98
  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
99
 
116
100
  /**
117
101
  * 依赖反转构造:backend 承担 appendEntry/pi.sendMessage/时间源,runtime 只持有内存态。
@@ -128,6 +112,13 @@ export class SchedulerRuntime {
128
112
  this.backend = backend
129
113
  this.isCtxStale = isCtxStale
130
114
  this.modelOps = modelOps
115
+ // U6c:session_tree(树回退)重折叠接线——生产 backend 在 pi.on('session_tree')
116
+ // 时按活跃路径重折叠任务集并回调;落点 = 本 tasks Map(loadTasks 换集)。
117
+ // 纯重建:不 dispatch、不 append op、不动 tickTimer 启停(常驻循环照常消费
118
+ // 新任务集,被撤子树任务不在集内即到点不触发)也不动 pendingModelSwitch
119
+ // (撤回编排前置 = 会话空闲,无在途模型切换记录)。测试替身不实现
120
+ // onSessionTree 时 ?. 订阅为 no-op,既有单测路径不变。
121
+ backend.onSessionTree?.((tasks) => this.loadTasks(tasks))
131
122
  }
132
123
 
133
124
  // ── 任务 CRUD ──
@@ -301,15 +292,15 @@ export class SchedulerRuntime {
301
292
  }
302
293
 
303
294
  async tickScheduler(): Promise<void> {
304
- // U4 对账兜底(设计 D3):严格先于本 tick 的 dispatch 循环。顺序约束:否则出现
305
- // 「B dispatch 先切 Y → 对账持 A 旧记录把 Y 静默切回」的交错(Y 从未生效、全序列
306
- // 无报错,G4 被绕过)。恢复执行时 await 完成,保证恢复 setModel 完成后 dispatch 循环
307
- // 才开 setModel;更广的「任意两个 setModel 不并发」由模型 op 串行队列(modelOpChain)
308
- // 结构保证——含 turn_end 恢复 fire-and-forget 挂点,dispatch 模型分支排在其后。
309
- // 无未决记录的绝大多数 tick 同步返回、不引入 microtask 断点——dispatch 的同步段
310
- // 契约(in-flight 守卫在 tickScheduler() 未 await 时同步可见)保持不变。
311
- const reconciling = this.reconcileModelSwitch()
312
- if (reconciling) await reconciling
295
+ // U4 未决模型切换记录的事实查询结算(严格先于本 tick 的 dispatch 循环)。顺序约束:
296
+ // 否则出现「B dispatch 先切 Y → 结算持 A 旧记录把 Y 静默切回」的交错(Y 从未生效、
297
+ // 全序列无报错,G4 被绕过)。恢复执行时 await 完成,保证恢复 setModel 完成后 dispatch
298
+ // 循环才开 setModel;更广的「任意两个 setModel 不并发」由模型 op 串行队列(modelOpChain)
299
+ // 结构保证——含 agent_settled 恢复 fire-and-forget 挂点,dispatch 模型分支排在其后。
300
+ // 无未决记录(或非 idle)的绝大多数 tick 同步返回、不引入 microtask 断点——dispatch
301
+ // 的同步段契约(in-flight 守卫在 tickScheduler() 未 await 时同步可见)保持不变。
302
+ const settling = this.settlePendingIfIdle('tick-reconcile')
303
+ if (settling) await settling
313
304
 
314
305
  const now = this.backend.now()
315
306
 
@@ -388,8 +379,8 @@ export class SchedulerRuntime {
388
379
  * 按序执行「互斥校验 → setModel(task.model) → 记未决切换记录 → sendMessage」,切换段
389
380
  * 整体入模型 op 串行队列(modelOpChain——与恢复 setModel 互斥,MF-2)。busy 亦照常 setModel
390
381
  * (P-MODEL-② 证伪「busy 切换不生效」——steer 消息消费时在同一 run 内开新 turn,turn
391
- * 开始从 ctx.model 取当前模型,setModel 对 steer turn 生效。pi 实装锚点(0.84.4):
392
- * dist/core/agent-session.js:1258 setModel 同步写 agent.state.model + :304 每个 turn
382
+ * 开始从 ctx.model 取当前模型,setModel 对 steer turn 生效。pi 实装锚点(1.0.0 复核):
383
+ * dist/core/agent-session.js setModel 同步写 agent.state.model + 每个 turn
393
384
  * 准备时从 agent.state.model 取模型快照——setModel 先于下一 turn 准备即生效)。
394
385
  * 失败降级(分级,不阻塞核心
395
386
  * 调度):modelOps 缺省 / 会话无当前模型 / setModel false → 日志 + 放弃切换,照常 dispatch。
@@ -410,18 +401,18 @@ export class SchedulerRuntime {
410
401
  if (this.pendingModelSwitch) {
411
402
  // 互斥窗口(设计 D3,理由 = 切换秩序):未决记录未关闭时,其他需切模型任务命中即
412
403
  // skip 本轮 + pending 留待下 tick 重试(复用现状 skip 语义,不建真队列;不需切模型
413
- // 的任务不受互斥影响——检查只在本分支)。「dispatch 写新记录前先强制结算旧记录」
414
- // 的落地:本分支结构性永不写新记录(杜绝新旧叠写),同时把已过窗口的 in-flight
415
- // 残留先强制开放转移,保证新任务在旧记录结算后 ≤1 tick 内接管、不被过期残留无限阻塞。
416
- // 强制结算的恢复必须 await(顺序约束,与 tickScheduler 的对账 await 同构):恢复
404
+ // 的任务不受互斥影响——检查只在本分支)。「dispatch 写新记录前先结算旧记录」
405
+ // 的落地:本分支结构性永不写新记录(杜绝新旧叠写),skip 前做一次 isIdle() 事实
406
+ // 查询结算(idle 即恢复收口),保证新任务不被旧记录无限阻塞。
407
+ // 结算的恢复必须 await(顺序约束,与 tickScheduler 的结算 await 同构):恢复
417
408
  // setModel(原) 完成后才放行 skip——恢复与紧随的需切模型任务 setModel(目标) 不得
418
409
  // 并发(串行由模型 op 队列结构保证,本 await 是调用方顺序语义:skip 放行前恢复
419
410
  // 已收口);本分支随后即 skip,await 无额外延迟代价。
420
- await this.forceSettleExpiredInFlight()
411
+ await this.settlePendingIfIdle('mutex-settle')
421
412
  skipped = true
422
413
  } else {
423
414
  // 模型 op 串行段(MF-2 收口,互斥窗口全程覆盖「setModel 起步 → 记录创建」):
424
- // 前序模型 op(turn_end 恢复 / 前序 dispatch 切换 / 错误路径恢复)可能仍在途——
415
+ // 前序模型 op(agent_settled 恢复 / 前序 dispatch 切换 / 错误路径恢复)可能仍在途——
425
416
  // 排队等待期间未决记录与当前模型均可能变化,禁止按排队前快照盲切(旧快照的
426
417
  // expectedModelRef 会把恢复锚点记错),串行段内重新校验后再切换。后来的需切模型
427
418
  // 任务同样排队、出队时重新校验命中互斥 skip——双记录叠写 / 两个 setModel 并发
@@ -448,17 +439,14 @@ export class SchedulerRuntime {
448
439
  const switched = await modelOps.setModelByRef(targetModelRef)
449
440
  if (switched) {
450
441
  // setModel 受理 → 记未决切换记录(会话期望模型 + 在途标记,内存态);
451
- // sendMessage 受理后等事件恢复(message_start 归属 → turn_end + isIdle 复核)。
442
+ // sendMessage 受理后等 run 落定(agent_settled + isIdle 复核)兑现恢复。
452
443
  // 记录创建收在同一串行段内:互斥窗口对后继模型任务即时可见,无「已切未记」
453
444
  // 的放行缺口。
454
445
  this.pendingModelSwitch = {
455
446
  taskId: task.id,
456
447
  expectedModelRef: currentNow,
457
448
  targetModelRef,
458
- phase: 'in-flight',
459
- ticksOpen: 0,
460
449
  }
461
- this.dispatchedTurnIndex = null
462
450
  } else {
463
451
  // setModel false(无可用 API key 等)→ 按「恢复失败」同族处理:日志 + 放弃本次
464
452
  // 切换,任务照常 dispatch(模型字段不阻塞核心调度)
@@ -489,7 +477,6 @@ export class SchedulerRuntime {
489
477
  const ps = this.pendingModelSwitch
490
478
  if (ps && modelOps) {
491
479
  this.pendingModelSwitch = null
492
- this.dispatchedTurnIndex = null
493
480
  // 恢复 setModel 入模型 op 串行队列(与其他 setModel 发起点互斥,不并发);排队后
494
481
  // 复核「当前 == 期望」:等待期间已切回则不动作
495
482
  await this.serializeModelOp(async () => {
@@ -552,23 +539,23 @@ export class SchedulerRuntime {
552
539
  return this.dispatchTimestamps.length < RATE_LIMIT_PER_MINUTE
553
540
  }
554
541
 
555
- // ── 模型切换:对账兜底与归属状态机(U4,设计 D3 修订版)──
542
+ // ── 模型切换:agent_settled 恢复与 tick 事实查询(U4,D3 修订版 + D1 归属简化
543
+ // + ADR-0122 事实查询翻转)──
556
544
 
557
545
  /**
558
- * 未决模型切换记录的对账探测(tickScheduler 开头调用,严格先于 dispatch 循环)。
559
- * 同步推进状态机,仅在需要执行恢复时返回 Promise(调用方 await——顺序约束)。
560
- * 对账守卫前置:仅当存在未决记录才比对恢复——无记录时「模型 ≠ 期望」是用户自主行为,
561
- * 不动作(否则用户在任务间隙手动切模型会被静默回滚)。
562
- * - in-flight:ticksOpen 计数超 MODEL_SWITCH_RECONCILE_TICKS → 恢复回调永不执行(事件
563
- * 丢失),强制开放:idle 即恢复(返回恢复 Promise);非 idle 转 awaiting-restore
564
- * (P-MODEL-② 证实 setModel 不影响 in-flight turn,但保守口径维持非 idle 推迟。
565
- * pi 实装锚点(0.84.4):dist/core/agent-session.js:304 turn 模型在准备时一次性
566
- * 快照,setModel 只写 state 不回写已在途 turn 的已快照模型)。
567
- * 窗口内继续等事件。
568
- * - awaiting-restore:比对决策已在窗口内锁定,执行不受窗口过期限制——idle 重入即兑现
569
- * (tick 为天然重入点,不设墙钟上界:任务级正常路径无墙钟超时)。
546
+ * 未决模型切换记录的事实查询结算:记录存在 + 会话 idle(isIdle() 同步查询,
547
+ * 判定层级最高的事实来源)→ 兑现恢复;非 idle → 不动作(不能把在途 run 的模型
548
+ * 换掉),记录保留等下一查询点。无记录时同步返回 undefined(调用方跳过 await,
549
+ * 保住 tick 无记录路径的同步契约——dispatch 的 in-flight 守卫在 tickScheduler()
550
+ * 未 await 时同步可见)。
551
+ *
552
+ * 调用点两处:① tickScheduler 开头(严格先于 dispatch 循环——顺序约束:否则
553
+ * 「B dispatch 先切 Y → 对账持 A 旧记录把 Y 静默切回」;agent_settled 事件丢失时
554
+ * 本查询点是正确性兜底,事件只作提前触发加速器);② dispatchTaskInner 互斥分支
555
+ * (skip 前尽量把旧记录收口,恢复 setModel 与后续新任务的 setModel(目标) 不得并发
556
+ * ——串行由模型 op 队列结构保证,本 await 是调用方顺序语义)。
570
557
  */
571
- private reconcileModelSwitch(): Promise<void> | undefined {
558
+ private settlePendingIfIdle(reason: string): Promise<void> | undefined {
572
559
  const ps = this.pendingModelSwitch
573
560
  if (!ps) return undefined
574
561
  if (this.isCtxStale?.()) {
@@ -578,44 +565,8 @@ export class SchedulerRuntime {
578
565
  }
579
566
  const ops = this.modelOps
580
567
  if (!ops) return undefined
581
- if (ps.phase === 'in-flight') {
582
- ps.ticksOpen += 1
583
- if (ps.ticksOpen <= MODEL_SWITCH_RECONCILE_TICKS) return undefined
584
- logger.warn('model switch in-flight mark expired, forcing reconciliation', {
585
- taskId: ps.taskId,
586
- expectedModelRef: ps.expectedModelRef,
587
- targetModelRef: ps.targetModelRef,
588
- })
589
- if (!ops.isIdle()) {
590
- ps.phase = 'awaiting-restore'
591
- return undefined
592
- }
593
- return this.restoreExpectedModel('reconcile-forced')
594
- }
595
568
  if (!ops.isIdle()) return undefined
596
- return this.restoreExpectedModel('reconcile-idle')
597
- }
598
-
599
- /**
600
- * 互斥命中时对已过窗口的 in-flight 残留做强制开放转移(idle 恢复 / 非 idle 转
601
- * awaiting-restore);未过窗口或已在 awaiting-restore 的记录不动(其结算由事件与
602
- * tick 对账通道负责)。
603
- *
604
- * 返回 Promise 而非 fire-and-forget:idle 恢复路径透传 restoreExpectedModel,调用方
605
- * (dispatchTaskInner 互斥分支)必须 await——恢复 setModel(原) 与后续新任务的
606
- * setModel(目标) 不得并发(runTaskNow 直连路径下完成顺序不定,恢复后完成则该任务
607
- * turn 用错模型;串行由模型 op 队列结构保证,本 await 是调用方顺序语义:skip 放行前
608
- * 恢复已收口);本分支随后即 skip,await 无额外延迟代价。
609
- */
610
- private forceSettleExpiredInFlight(): Promise<void> {
611
- const ps = this.pendingModelSwitch
612
- if (!ps || !this.modelOps) return Promise.resolve()
613
- if (ps.phase !== 'in-flight' || ps.ticksOpen <= MODEL_SWITCH_RECONCILE_TICKS) return Promise.resolve()
614
- if (this.modelOps.isIdle()) {
615
- return this.restoreExpectedModel('mutex-forced')
616
- }
617
- ps.phase = 'awaiting-restore'
618
- return Promise.resolve()
569
+ return this.restoreExpectedModel(reason)
619
570
  }
620
571
 
621
572
  /**
@@ -638,8 +589,8 @@ export class SchedulerRuntime {
638
589
  * 执行恢复(切回会话期望模型)并关闭未决记录(在途标记生命周期收口):
639
590
  * 当前模型已等于期望(用户已手动切回)→ 清记录不动作;setModel(原) false →
640
591
  * restore-failed 终态(日志含恢复动作:会话停留在任务模型,需手动切回)+ 清记录。
641
- * 记录先关后恢复——防 await setModel 期间 turn_end 与 tick 对账并发重入。
642
- * 恢复 setModel 本体经模型 op 串行队列(serializeModelOp,MF-2):turn_end 挂点的
592
+ * 记录先关后恢复——防 await setModel 期间 agent_settled 与 tick 对账并发重入。
593
+ * 恢复 setModel 本体经模型 op 串行队列(serializeModelOp,MF-2):agent_settled 挂点的
643
594
  * fire-and-forget 调用由此变为安全——在途恢复持队,后续任何 setModel 发起点
644
595
  * (dispatch 切换 / 对账恢复)排队等待,两个 setModel 并发结构性不可能(完成顺序
645
596
  * 不定 → turn 静默跑错模型的交错被消除)。「当前 == 期望」复核移入串行段:排队
@@ -650,7 +601,6 @@ export class SchedulerRuntime {
650
601
  const ops = this.modelOps
651
602
  if (!ps || !ops) return Promise.resolve()
652
603
  this.pendingModelSwitch = null
653
- this.dispatchedTurnIndex = null
654
604
  return this.serializeModelOp(async () => {
655
605
  if (ops.getCurrentModelRef() === ps.expectedModelRef) return
656
606
  const restored = await ops.setModelByRef(ps.expectedModelRef)
@@ -665,85 +615,28 @@ export class SchedulerRuntime {
665
615
  })
666
616
  }
667
617
 
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
- }
618
+ // 模型恢复事件入口(index.ts 的 pi.on('agent_settled') 转发;方法内不触碰捕获的 pi/ctx
619
+ // ——恢复动作经构造注入的 modelOps,stale 代际由 isCtxStale 入口检查拦下:清状态机、
620
+ // 不执行恢复)。
685
621
 
686
622
  /**
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。
691
- */
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 {
706
- if (this.isCtxStale?.()) return
707
- 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')
718
- } else {
719
- ps.phase = 'awaiting-restore'
720
- }
721
- }
722
-
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,兜底语义不变。
623
+ * agent_settled:模型恢复的事件触发点(D1 归属简化——原 turnIndex 归属:4 状态字段 +
624
+ * 6 事件挂点 + customType 前缀匹配,绕路自造对 2-tick 对账的依赖)。settled = run
625
+ * 完全落定(无 retry/compaction/queued continuation;pi 实装 agent-session.js
626
+ * _emitAgentSettled 先置 _isAgentRunActive = false 再 emit),dispatched turn 必已
627
+ * 结束,无需 turnIndex 归属识别——事件到达即做 isIdle() 事实查询:
628
+ * - idle → 兑现恢复(事件是提前触发的加速器,恢复延迟 = 事件即时)
629
+ * - 非 idle(settled 与用户新 run 交错)→ 不能切:会把新 run 的模型换掉
630
+ * (P-MODEL-② 同族形态)。记录保留,由 tick 的 isIdle() 查询在 idle 重入时兑现。
631
+ * 不挂 agent_end 恢复:end 后仍可能有自动续跑 turn,此时切回会把续跑 turn 的模型换掉
632
+ * (P-MODEL-② 同族形态)——恢复只认 settled。
740
633
  */
741
634
  handleRunSettled(): void {
742
635
  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')
636
+ if (!this.pendingModelSwitch || !this.modelOps) return
637
+ if (this.modelOps.isIdle()) {
638
+ void this.restoreExpectedModel('agent-settled')
639
+ }
747
640
  }
748
641
 
749
642
  /**
@@ -760,7 +653,10 @@ export class SchedulerRuntime {
760
653
 
761
654
  // ── 装配与回调 ──
762
655
 
763
- /** 装配点注入初始任务数组(读盘/重放由 backend 完成,runtime 只持有内存态)。 */
656
+ /**
657
+ * 任务集换装入口(装配点 session_start 注入初始数组 + U6c session_tree 重折叠共用;
658
+ * 读盘/重放由 backend 完成,runtime 只持有内存态)。
659
+ */
764
660
  loadTasks(tasks: ScheduledTask[]): void {
765
661
  this.tasks = new Map(tasks.map(t => [t.id, t]))
766
662
  }
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:'