@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/package.json +4 -3
- package/src/__tests__/U4-MODEL-SWITCH.test.ts +39 -166
- package/src/__tests__/active-path.test.ts +253 -0
- package/src/__tests__/index-generation.test.ts +1 -2
- package/src/__tests__/runtime.test.ts +6 -14
- package/src/__tests__/session-tree-registration.test.ts +152 -0
- package/src/__tests__/widget-push.test.ts +1 -2
- package/src/backend.ts +53 -3
- package/src/index.ts +34 -17
- package/src/runtime.ts +63 -111
- package/src/types.ts +1 -1
|
@@ -11,7 +11,7 @@ vi.mock('@zhushanwen/pi-extension-logger', () => ({
|
|
|
11
11
|
}))
|
|
12
12
|
|
|
13
13
|
import { MockSchedulerBackend } from './mock-backend.js'
|
|
14
|
-
import { SchedulerRuntime } from '../runtime.js'
|
|
14
|
+
import { TICK_INTERVAL_MS, SchedulerRuntime } from '../runtime.js'
|
|
15
15
|
|
|
16
16
|
// MockSchedulerBackend 零 FS 副作用:runtime 不再触碰 store,无需 mock store.js。
|
|
17
17
|
|
|
@@ -48,16 +48,8 @@ describe('SchedulerRuntime', () => {
|
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
describe('listTasks', () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await runtime.addTask('task 2', { mode: 'interval', intervalMs: 30000 })
|
|
54
|
-
const tasks = runtime.listTasks()
|
|
55
|
-
expect(tasks).toHaveLength(2)
|
|
56
|
-
// 30s interval 的 nextRunAt 早于 60s 的,应排前
|
|
57
|
-
expect(tasks[0]!.nextRunAt).toBeLessThan(tasks[1]!.nextRunAt)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
// 强化断言:30s 任务 nextRunAt 更小(更早),应是 listTasks()[0]
|
|
51
|
+
// 排序契约:30s 任务 nextRunAt 更小(更早),应是 listTasks()[0]
|
|
52
|
+
//(含排序断言全集:tasks[0].id 定位 + nextRunAt 严格小于)
|
|
61
53
|
it('orders shorter-interval task first', async () => {
|
|
62
54
|
const t60 = await runtime.addTask('60s', { mode: 'interval', intervalMs: 60000 })
|
|
63
55
|
const t30 = await runtime.addTask('30s', { mode: 'interval', intervalMs: 30000 })
|
|
@@ -115,7 +107,9 @@ describe('SchedulerRuntime', () => {
|
|
|
115
107
|
const task = await runtime.addTask('test', { mode: 'interval', intervalMs: 60000 })
|
|
116
108
|
await runtime.dispatchTask(task)
|
|
117
109
|
expect(backend.sentMessages).toHaveLength(1)
|
|
118
|
-
expect(backend.sentMessages[0]!.msg).toEqual(expect.objectContaining({ content: 'test' }))
|
|
110
|
+
expect(backend.sentMessages[0]!.msg).toEqual(expect.objectContaining({ content: 'test', customType: 'pi-scheduler:dispatched' }))
|
|
111
|
+
// steer 直投契约(runtime.ts sendMessage 第二参):丢 opts = pi idle 时定时任务静默不开轮次
|
|
112
|
+
expect(backend.sentMessages[0]!.opts).toEqual({ deliverAs: 'steer', triggerTurn: true })
|
|
119
113
|
})
|
|
120
114
|
|
|
121
115
|
it('skips disabled task', async () => {
|
|
@@ -565,7 +559,6 @@ describe('SchedulerRuntime', () => {
|
|
|
565
559
|
// 自停;其他错误 → warn "tick error" 继续调度。修复前 tick 内异常无人接住 →
|
|
566
560
|
// unhandledRejection → pi 主进程 exit 1。
|
|
567
561
|
describe('tick 错误分诊(F2)', () => {
|
|
568
|
-
const TICK_INTERVAL_MS = 30_000
|
|
569
562
|
|
|
570
563
|
beforeEach(() => {
|
|
571
564
|
vi.useFakeTimers()
|
|
@@ -639,7 +632,6 @@ describe('SchedulerRuntime', () => {
|
|
|
639
632
|
// clearExtensionCache 后 jiti 重 import 全新模块环境,旧闭包的模块级代数冻结不再递增,
|
|
640
633
|
// 只剩文案能识别 stale;模块级方案的装配级验证见 index-generation.test.ts factory 重跑用例)
|
|
641
634
|
describe('G1: 代际检测分诊(S9)', () => {
|
|
642
|
-
const TICK_INTERVAL_MS = 30_000
|
|
643
635
|
let staleFlag: boolean
|
|
644
636
|
let genRuntime: SchedulerRuntime
|
|
645
637
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// src/__tests__/session-tree-registration.test.ts
|
|
2
|
+
//
|
|
3
|
+
// U6c session_tree 注册面契约:注册在 extension factory 顶层(每代恰一 handler),
|
|
4
|
+
// 同代 session_start 复发不累积。回归背景:注册曾在 PiSchedulerBackend 构造函数内,
|
|
5
|
+
// 而 pi RPC 模式下每次 session 替换 session_start 在同一代内触发两次
|
|
6
|
+
// (agent-session-runtime finishSessionReplacement 内部 rebindSession + RPC handler
|
|
7
|
+
// 再 rebindSession,0.84.4 实装核对),pi 的 on 是追加语义(loader.js list.push,
|
|
8
|
+
// 无去重无 off)——handler 随替换线性累积,残留旧代 backend 在旧 ctx 上重复执行。
|
|
9
|
+
//
|
|
10
|
+
// mock pi 的 on 必须是追加语义(list.push,对齐 pi 实装):覆盖式 Map.set 会掩盖
|
|
11
|
+
// 累积回归,正是本套件要防的形态。
|
|
12
|
+
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
14
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
15
|
+
|
|
16
|
+
// MF-3(与 sdk-contract.test.ts 同款):mock importer,session_start 装配零 FS 副作用。
|
|
17
|
+
vi.mock('../importer.js', () => ({ importLegacyStore: vi.fn(() => vi.fn()) }))
|
|
18
|
+
|
|
19
|
+
// mock 共享 logger(与 widget-push.test.ts 同款):防测试触碰真实日志目录。
|
|
20
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
21
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
22
|
+
}))
|
|
23
|
+
vi.mock('@zhushanwen/pi-extension-logger', () => ({
|
|
24
|
+
getLogger: () => loggerMock,
|
|
25
|
+
createLogger: () => loggerMock,
|
|
26
|
+
setPiHandle: vi.fn(),
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
import schedulerExtension from '../index.js'
|
|
30
|
+
import { TASK_ENTRY_TYPE } from '../types.js'
|
|
31
|
+
|
|
32
|
+
type Handler = (...args: unknown[]) => unknown
|
|
33
|
+
|
|
34
|
+
/** 追加语义 mock pi:on = list.push(对齐 pi loader.js on 实装,无去重无 off)。 */
|
|
35
|
+
function createAppendPi(): {
|
|
36
|
+
pi: ExtensionAPI
|
|
37
|
+
handlers: Map<string, Handler[]>
|
|
38
|
+
tools: { name: string; execute: (...args: unknown[]) => Promise<{ content: { text: string }[] }> }[]
|
|
39
|
+
} {
|
|
40
|
+
const handlers = new Map<string, Handler[]>()
|
|
41
|
+
const tools: { name: string; execute: (...args: unknown[]) => Promise<{ content: { text: string }[] }> }[] = []
|
|
42
|
+
const pi = {
|
|
43
|
+
registerTool: (tool: { name: string; execute: (...args: unknown[]) => Promise<{ content: { text: string }[] }> }) =>
|
|
44
|
+
tools.push(tool),
|
|
45
|
+
registerCommand: vi.fn(),
|
|
46
|
+
on: (event: string, handler: Handler) => {
|
|
47
|
+
const list = handlers.get(event) ?? []
|
|
48
|
+
list.push(handler)
|
|
49
|
+
handlers.set(event, list)
|
|
50
|
+
},
|
|
51
|
+
sendMessage: vi.fn(),
|
|
52
|
+
appendEntry: vi.fn(),
|
|
53
|
+
} as unknown as ExtensionAPI
|
|
54
|
+
return { pi, handlers, tools }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 远期到期的启用任务 upsert entry(owner = sessionFile;测试窗内无 dispatch 噪音)。 */
|
|
58
|
+
function taskEntry(sessionFile: string, taskId: string) {
|
|
59
|
+
return {
|
|
60
|
+
type: 'custom',
|
|
61
|
+
customType: TASK_ENTRY_TYPE,
|
|
62
|
+
data: {
|
|
63
|
+
op: 'upsert',
|
|
64
|
+
taskId,
|
|
65
|
+
ownerSessionFile: sessionFile,
|
|
66
|
+
task: {
|
|
67
|
+
id: taskId,
|
|
68
|
+
name: taskId,
|
|
69
|
+
prompt: `${taskId} prompt`,
|
|
70
|
+
kind: 'recurring',
|
|
71
|
+
schedule: { mode: 'interval', intervalMs: 3_600_000 },
|
|
72
|
+
enabled: true,
|
|
73
|
+
createdAt: 0,
|
|
74
|
+
nextRunAt: Date.now() + 3_600_000,
|
|
75
|
+
runCount: 0,
|
|
76
|
+
history: [],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** fake ctx:entries 经容器可变(模拟撤回前后 swap);ui.setWidget 观测面。 */
|
|
83
|
+
function createFakeCtx(sessionFile: string, entriesBox: { entries: unknown[] }): ExtensionContext {
|
|
84
|
+
return {
|
|
85
|
+
cwd: '/test-tree-reg',
|
|
86
|
+
isIdle: () => true,
|
|
87
|
+
hasPendingMessages: () => false,
|
|
88
|
+
ui: { setWidget: vi.fn() },
|
|
89
|
+
sessionManager: {
|
|
90
|
+
getEntries: () => entriesBox.entries,
|
|
91
|
+
getSessionFile: () => sessionFile,
|
|
92
|
+
},
|
|
93
|
+
} as unknown as ExtensionContext
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('session_tree 注册面(factory 顶层恰一,同代 session_start 复发不累积)', () => {
|
|
97
|
+
it('factory 运行恰注册 1 个 session_tree handler', () => {
|
|
98
|
+
const { pi, handlers } = createAppendPi()
|
|
99
|
+
schedulerExtension(pi)
|
|
100
|
+
expect(handlers.get('session_tree')).toHaveLength(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('同代 session_start 复发(RPC 模式每次替换触发 2 次)→ session_tree handler 恒恰 1', () => {
|
|
104
|
+
const { pi, handlers } = createAppendPi()
|
|
105
|
+
schedulerExtension(pi)
|
|
106
|
+
const entriesBox = { entries: [] as unknown[] }
|
|
107
|
+
const ctx = createFakeCtx('/test/tree-reg.json', entriesBox)
|
|
108
|
+
const sessionStart = handlers.get('session_start')![0]!
|
|
109
|
+
|
|
110
|
+
sessionStart({ type: 'session_start', reason: 'new' }, ctx)
|
|
111
|
+
sessionStart({ type: 'session_start', reason: 'resume' }, ctx)
|
|
112
|
+
|
|
113
|
+
expect(handlers.get('session_tree')).toHaveLength(1)
|
|
114
|
+
|
|
115
|
+
// 收尾停当代 runtime 的 tick interval(fake timers 未启用,interval 需显式清)
|
|
116
|
+
void handlers.get('session_shutdown')![0]()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('复发后 session_tree 委托当代 backend:撤回后重折叠反映到任务查询', () => {
|
|
120
|
+
const file = '/test/tree-reg-delegate.json'
|
|
121
|
+
const entriesBox = { entries: [taskEntry(file, 't1')] as unknown[] }
|
|
122
|
+
const { pi, handlers, tools } = createAppendPi()
|
|
123
|
+
schedulerExtension(pi)
|
|
124
|
+
const ctx = createFakeCtx(file, entriesBox)
|
|
125
|
+
const sessionStart = handlers.get('session_start')![0]!
|
|
126
|
+
|
|
127
|
+
// 同代复发两次:第 2 个 backend 为当代(旧代已被 stopScheduler + 代数判 stale)
|
|
128
|
+
sessionStart({ type: 'session_start', reason: 'new' }, ctx)
|
|
129
|
+
sessionStart({ type: 'session_start', reason: 'resume' }, ctx)
|
|
130
|
+
|
|
131
|
+
// 基线:t1 在当代任务集内
|
|
132
|
+
const control = tools.find(t => t.name === 'schedule_control')!
|
|
133
|
+
|
|
134
|
+
return control
|
|
135
|
+
.execute('call-base', { action: 'list' }, undefined, undefined, ctx)
|
|
136
|
+
.then(result => {
|
|
137
|
+
expect(result.content[0]!.text).toContain('t1')
|
|
138
|
+
|
|
139
|
+
// 撤回:entry 移出(getEntries 变化)→ session_tree 事件 → 当代 backend 重折叠
|
|
140
|
+
entriesBox.entries = []
|
|
141
|
+
handlers.get('session_tree')![0]!({ type: 'session_tree' }, ctx)
|
|
142
|
+
|
|
143
|
+
return control.execute('call-after', { action: 'list' }, undefined, undefined, ctx)
|
|
144
|
+
})
|
|
145
|
+
.then(result => {
|
|
146
|
+
expect(result.content[0]!.text).not.toContain('t1')
|
|
147
|
+
})
|
|
148
|
+
.finally(() => {
|
|
149
|
+
void handlers.get('session_shutdown')![0]()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -35,9 +35,8 @@ vi.mock('@zhushanwen/pi-extension-logger', () => ({
|
|
|
35
35
|
}))
|
|
36
36
|
|
|
37
37
|
import schedulerExtension from '../index.js'
|
|
38
|
+
import { TICK_INTERVAL_MS } from '../runtime.js'
|
|
38
39
|
import { TASK_ENTRY_TYPE } from '../types.js'
|
|
39
|
-
|
|
40
|
-
const TICK_INTERVAL_MS = 30_000
|
|
41
40
|
/** 与 index.ts 的 WIDGET_KEEPALIVE_INTERVAL_MS 同值(10min;常量不导出,测试锚定同值)。 */
|
|
42
41
|
const KEEPALIVE_INTERVAL_MS = 10 * 60 * 1000
|
|
43
42
|
const BASE = Date.parse('2026-01-01T00:00:00Z')
|
package/src/backend.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import { filterActivePath } from '@zhushanwen/pi-session-path'
|
|
2
3
|
|
|
3
4
|
import { replayFoldEntries, type SchedulerEntryLike } from './replay.js'
|
|
4
5
|
import { TASK_ENTRY_TYPE } from './types.js'
|
|
@@ -70,6 +71,14 @@ export interface SchedulerBackend {
|
|
|
70
71
|
* 不触发任何写入(项目规则 #6)。
|
|
71
72
|
*/
|
|
72
73
|
getEntries(): SchedulerEntryLike[]
|
|
74
|
+
/**
|
|
75
|
+
* U6c:session_tree(树回退)重折叠订阅。生产注册面在 extension factory 顶层
|
|
76
|
+
* (index.ts:pi.on('session_tree') 每代恰一,事件经 activeBackend 委托
|
|
77
|
+
* PiSchedulerBackend.refoldSessionTree),事件到达时按活跃路径重折叠任务集并回调
|
|
78
|
+
* listener——落点是 SchedulerRuntime 的内存任务 Map(runtime 构造时订阅,backend
|
|
79
|
+
* 不能反向持有 runtime)。测试替身可不实现(构造侧 ?. 订阅为 no-op,既有单测路径不变)。
|
|
80
|
+
*/
|
|
81
|
+
onSessionTree?(listener: (tasks: ScheduledTask[]) => void): void
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
/**
|
|
@@ -81,6 +90,14 @@ export interface SchedulerBackendCtx {
|
|
|
81
90
|
sessionManager: {
|
|
82
91
|
getEntries(): SchedulerEntryLike[]
|
|
83
92
|
getSessionFile(): string | undefined
|
|
93
|
+
/**
|
|
94
|
+
* 当前叶子 entry id(U6c 活跃路径回溯锚)。可选:真实 ExtensionContext 恒存在
|
|
95
|
+
* (ReadonlySessionManager Pick 清单含 getLeafId);最小 duck-typed ctx(单测)
|
|
96
|
+
* 缺失时按文件尾回退——session_start 时点与 pi 树重放规则(叶子 = 文件最后
|
|
97
|
+
* 一条 entry)等价,session_tree 时点由生产 ctx 携带精确锚。
|
|
98
|
+
* (pi 树重放规则实锚登记 pi-semantics PS-60:dist/core/session-manager.js:673 _buildIndex)
|
|
99
|
+
*/
|
|
100
|
+
getLeafId?(): string | null
|
|
84
101
|
}
|
|
85
102
|
/**
|
|
86
103
|
* 会话是否空闲(not streaming)。真实 ExtensionContext 保证存在(SDK
|
|
@@ -96,6 +113,14 @@ export interface SchedulerBackendCtx {
|
|
|
96
113
|
model?: SchedulerCurrentModel
|
|
97
114
|
}
|
|
98
115
|
|
|
116
|
+
// ── 活跃路径裁剪(U6c)──
|
|
117
|
+
|
|
118
|
+
// 活跃路径裁剪(leafId 沿 parentId 回溯,撤回后被撤子树的任务 op 不进折叠输入,
|
|
119
|
+
// 被撤任务不得恢复 pending、到点触发真实 turn——A14)收敛于
|
|
120
|
+
// @zhushanwen/pi-session-path 单一实现(四包同构副本收编,防御语义与回退口径见
|
|
121
|
+
// 该包 filterActivePath 注释;runtime 侧 entry-tree-builder 保持独立——extension
|
|
122
|
+
// 不能 import runtime 包)。
|
|
123
|
+
|
|
99
124
|
// ── 生产实现 ──
|
|
100
125
|
|
|
101
126
|
/**
|
|
@@ -111,22 +136,47 @@ export class PiSchedulerBackend implements SchedulerBackend {
|
|
|
111
136
|
ExtensionAPI,
|
|
112
137
|
'sendMessage' | 'appendEntry' | 'registerProvider' | 'unregisterProvider'
|
|
113
138
|
>
|
|
139
|
+
private sessionTreeListener: ((tasks: ScheduledTask[]) => void) | null = null
|
|
114
140
|
|
|
115
141
|
constructor(
|
|
116
142
|
ctx: SchedulerBackendCtx,
|
|
117
|
-
pi: Pick<
|
|
143
|
+
pi: Pick<
|
|
144
|
+
ExtensionAPI,
|
|
145
|
+
'sendMessage' | 'appendEntry' | 'registerProvider' | 'unregisterProvider'
|
|
146
|
+
>,
|
|
118
147
|
) {
|
|
119
148
|
this.ctx = ctx
|
|
120
149
|
this.pi = pi
|
|
121
150
|
}
|
|
122
151
|
|
|
152
|
+
/** U6c:SchedulerRuntime 构造时订阅重折叠回调(见 SchedulerBackend.onSessionTree)。 */
|
|
153
|
+
onSessionTree(listener: (tasks: ScheduledTask[]) => void): void {
|
|
154
|
+
this.sessionTreeListener = listener
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* U6c:session_tree(树回退)事件委托体,由 extension factory 顶层注册的 handler
|
|
159
|
+
* 经 activeBackend 调用(index.ts;不在本类构造函数注册,注册面理由见该处注释)。
|
|
160
|
+
* 纯重建体:只按活跃路径重折叠任务集(loadTasks)并回调 runtime 换 Map——不做
|
|
161
|
+
* 任何 sendMessage / appendEntry / tickTimer 启停(tick 常驻循环照常消费新任务集,
|
|
162
|
+
* 被撤子树任务不在集内即到点不触发,A14)。
|
|
163
|
+
*/
|
|
164
|
+
refoldSessionTree(): void {
|
|
165
|
+
this.sessionTreeListener?.(this.loadTasks())
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
/**
|
|
124
169
|
* 读路径:折叠当前 session 的 TASK_ENTRY_TYPE custom entries 恢复任务(非接口成员,
|
|
125
|
-
* 由装配点 session_start
|
|
170
|
+
* 由装配点 session_start / session_tree 重折叠调用)。replayFoldEntries 内部含 fork
|
|
171
|
+
* owner 过滤与异常兜底。U6c:折叠输入接活跃路径裁剪——从 leafId 沿 parentId 回溯,
|
|
172
|
+
* 被撤子树的任务 op 不再恢复 pending(否则被撤任务到点照常触发真实 turn,A14)。
|
|
126
173
|
*/
|
|
127
174
|
loadTasks(): ScheduledTask[] {
|
|
128
175
|
return [
|
|
129
|
-
...replayFoldEntries(
|
|
176
|
+
...replayFoldEntries(
|
|
177
|
+
filterActivePath(this.ctx.sessionManager),
|
|
178
|
+
this.ctx.sessionManager.getSessionFile(),
|
|
179
|
+
).values(),
|
|
130
180
|
]
|
|
131
181
|
}
|
|
132
182
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
2
2
|
import type { GuiContext } from '@zhushanwen/extension-protocol'
|
|
3
3
|
import { toErrorMessage } from '@zhushanwen/pi-ext-guards'
|
|
4
4
|
import { getLogger } from '@zhushanwen/pi-extension-logger'
|
|
@@ -70,6 +70,9 @@ const WIDGET_KEEPALIVE_INTERVAL_MS = WIDGET_KEEPALIVE_MINUTES * MS_PER_MINUTE
|
|
|
70
70
|
*/
|
|
71
71
|
export default function schedulerExtension(pi: ExtensionAPI): void {
|
|
72
72
|
let service: SchedulerService | null = null
|
|
73
|
+
// U6c:当代 backend(session_start 装配点更新)。factory 顶层 session_tree handler
|
|
74
|
+
// 经它委托当代重折叠(注册面理由见下方 session_tree 注册点注释)。
|
|
75
|
+
let activeBackend: PiSchedulerBackend | null = null
|
|
73
76
|
// IMPORT-FLUSH-GUARD(MF-1):importLegacyStore 对未 flush 的新 session 返回延迟删除 .imported
|
|
74
77
|
// 的 cleanup——turn_end / session_shutdown 时执行:确认 flush(sessionFile 已出现)则删,
|
|
75
78
|
// 未 flush 保留供崩溃恢复重导入(否则未 flush 即退出 → 全部旧任务丢失且源文件已销毁)。
|
|
@@ -105,6 +108,9 @@ export default function schedulerExtension(pi: ExtensionAPI): void {
|
|
|
105
108
|
service?.runtime.stopScheduler()
|
|
106
109
|
// 装配点:backend(ctx.sessionManager 读 entries / pi.appendEntry 写 op)→ runtime(内存态 + 调度)→ service(业务入口)
|
|
107
110
|
const backend = new PiSchedulerBackend(ctx, pi)
|
|
111
|
+
// U6c:同步给 factory 顶层 session_tree handler(下方注册)——同代 session_start
|
|
112
|
+
// 复发时本赋值覆盖为最新 backend,委托恒指当代 ctx 的实例。
|
|
113
|
+
activeBackend = backend
|
|
108
114
|
// 旧 store 原子导入(CL3 方案A):必须在 backend.loadTasks() 之前执行——
|
|
109
115
|
// append 的 upsert entry 进入 pi 内存 fileEntries,紧接的 loadTasks replay 统一重放读到导入任务。
|
|
110
116
|
// ctx.cwd 类型为 string(SDK ExtensionContext 必填),无需 ?? process.cwd() 兜底(CL2)。
|
|
@@ -186,37 +192,48 @@ export default function schedulerExtension(pi: ExtensionAPI): void {
|
|
|
186
192
|
refreshWidget(ctx)
|
|
187
193
|
})
|
|
188
194
|
|
|
195
|
+
// U6c:树回退(撤回 __taiji_nav__ → navigateTree / 用户树跳转)后重折叠任务集。
|
|
196
|
+
// 注册在 factory 顶层而非 PiSchedulerBackend 构造函数:pi 的 on 是追加语义(loader.js
|
|
197
|
+
// on 实现 list.push,无去重无 off,0.84.4 实装核对),且 RPC 模式下每次 session 替换
|
|
198
|
+
// session_start 在同一代内触发两次(agent-session-runtime finishSessionReplacement
|
|
199
|
+
// 内部 rebindSession 一路 + RPC handler 再 rebindSession 一路,均经 bindExtensions
|
|
200
|
+
// emit session_start)——构造函数注册会同代线性累积 handler(残留旧代 backend 在旧
|
|
201
|
+
// ctx 上重复执行幂等 loadTasks,handler 数随替换无界缓增)。factory 每代恰好运行一次
|
|
202
|
+
// (loader.js initializeExtension 每代新建 handlers Map 并重跑 factory),顶层注册
|
|
203
|
+
// 结构性保证每代恰一 handler;委托 activeBackend(session_start 装配点更新)保证折叠
|
|
204
|
+
// 输入恒为当代实例,事件早于首个 session_start 时 no-op。
|
|
205
|
+
// 纯重建体(goal/plan/todo 的 session_tree handler 同款):只按活跃路径重折叠任务集
|
|
206
|
+
// 并回调 runtime 换 Map——不做任何 sendMessage / appendEntry / tickTimer 启停(tick
|
|
207
|
+
// 常驻循环照常消费新任务集,被撤子树任务不在集内即到点不触发,A14)。
|
|
208
|
+
pi.on('session_tree', (_event: unknown, _ctx: ExtensionContext) => {
|
|
209
|
+
activeBackend?.refoldSessionTree()
|
|
210
|
+
})
|
|
211
|
+
|
|
189
212
|
// turn_end 单注册共用(U4 恢复挂点与 MF-1 cleanup 同事件):真实 pi 的 on 是 handler 列表
|
|
190
213
|
// 追加,但测试仿真 mock 为覆盖式单 handler,且同事件单注册与「listener 防重复注册」纪律一致。
|
|
191
|
-
|
|
214
|
+
// (U4 模型恢复不在本事件:D1 归属简化后恢复唯一事件通道 = agent_settled,见 runtime.handleRunSettled)
|
|
215
|
+
pi.on('turn_end', () => {
|
|
192
216
|
// IMPORT-FLUSH-GUARD(MF-1):延迟删除的主触发点——turn_end 前该轮所有 message_end 已持久化
|
|
193
217
|
// (agent-session.js _handleAgentEvent 在 message_end 处理中调 appendMessage 触发 flush),
|
|
194
218
|
// sessionFile 已出现 → cleanup 删 .imported;仍未 flush(无 assistant 消息的轮次)→ 静默保留,
|
|
195
219
|
// 下次 turn_end / session_shutdown 重试。cleanup 幂等(importer.ts importFromFile)。
|
|
196
220
|
importCleanup?.()
|
|
197
|
-
// U4 dispatch 模型切换恢复挂点(设计 D3 修订版):状态机、恢复动作与 stale 代际守卫都在
|
|
198
|
-
// SchedulerRuntime。`event?.` 容错:pi 契约 payload 恒在,测试仿真可无参调用,缺省不匹配不动作。
|
|
199
|
-
service?.runtime.handleTurnEnd(event?.turnIndex)
|
|
200
221
|
// ack 安全网注销(幂等):正常路径已在 streamSimple 调用点自撤,这里覆盖「覆写未被调用」
|
|
201
222
|
// 的轮次(E2)。
|
|
202
223
|
ackController?.handleTurnEnd()
|
|
203
224
|
})
|
|
204
225
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
pi.on('agent_start', () => service?.runtime.handleAgentStart())
|
|
209
|
-
pi.on('turn_start', (event) => service?.runtime.handleTurnStart(event?.turnIndex))
|
|
226
|
+
// message_start 仅剩 ack 触发器判别(u-ack-turn):只有我们注入的 custom 消息(前缀
|
|
227
|
+
// pi-scheduler-ack:)才同步武装覆写;外来/assistant 消息一律忽略。(U4 dispatch 归属匹配
|
|
228
|
+
// 已随 D1 归属简化删除)
|
|
210
229
|
pi.on('message_start', (event) => {
|
|
211
|
-
service?.runtime.handleMessageStart(event?.message)
|
|
212
|
-
// ack 触发器判别(u-ack-turn):只有我们注入的 custom 消息(前缀 pi-scheduler-ack:)
|
|
213
|
-
// 才同步武装覆写;外来/assistant 消息一律忽略。
|
|
214
230
|
ackController?.handleMessageStart(event?.message)
|
|
215
231
|
})
|
|
216
|
-
|
|
217
|
-
// agent_settled
|
|
218
|
-
//
|
|
219
|
-
// runtime
|
|
232
|
+
|
|
233
|
+
// U4 dispatch 模型切换恢复的唯一事件挂点:agent_settled = run 完全沉降(无 retry/
|
|
234
|
+
// compaction/queued continuation)后的 isIdle 复核兑现(D1 归属简化——不挂 agent_start/
|
|
235
|
+
// turn_start/message_start/turn_end/agent_end,runtime 无 turnIndex 归属状态机;end 后仍
|
|
236
|
+
// 可能有自动续跑 turn,恢复只认 settled,见 runtime.handleRunSettled 注释)。
|
|
220
237
|
pi.on('agent_settled', () => service?.runtime.handleRunSettled())
|
|
221
238
|
|
|
222
239
|
pi.on('session_shutdown', async () => {
|