@zhushanwen/pi-goal 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -12
- package/package.json +8 -4
- package/src/__tests__/command-adapter.test.ts +404 -0
- package/src/__tests__/deserialize-state.test.ts +80 -0
- package/src/__tests__/event-adapter.test.ts +339 -0
- package/src/__tests__/goal-control-adapter.test.ts +303 -0
- package/src/__tests__/index.test.ts +119 -0
- package/src/__tests__/service.test.ts +317 -0
- package/src/__tests__/session.test.ts +220 -0
- package/src/__tests__/stale-checker.test.ts +78 -0
- package/src/__tests__/stubs/pi-sdk.ts +16 -0
- package/src/__tests__/stubs/typebox.ts +14 -0
- package/src/adapters/command-adapter.ts +360 -0
- package/src/adapters/event-adapter.ts +17 -0
- package/src/adapters/event-handlers/agent-end.ts +177 -0
- package/src/adapters/event-handlers/agent-start.ts +17 -0
- package/src/adapters/event-handlers/before-agent-start.ts +115 -0
- package/src/adapters/event-handlers/message-end.ts +37 -0
- package/src/adapters/event-handlers/session-start.ts +24 -0
- package/src/adapters/event-handlers/shared.ts +46 -0
- package/src/adapters/event-handlers/turn-end.ts +37 -0
- package/src/adapters/goal-control-adapter.ts +296 -0
- package/src/adapters/ports.ts +89 -0
- package/src/commands.ts +7 -19
- package/src/constants.ts +1 -3
- package/src/engine/__tests__/budget.test.ts +182 -0
- package/src/engine/__tests__/goal.test.ts +126 -0
- package/src/engine/budget.ts +146 -0
- package/src/engine/goal.ts +62 -0
- package/src/engine/types.ts +80 -0
- package/src/index.ts +110 -333
- package/src/persistence.ts +83 -0
- package/src/ports.ts +68 -0
- package/src/projection/__tests__/prompts.test.ts +245 -0
- package/src/projection/__tests__/widget.test.ts +255 -0
- package/src/projection/prompts.ts +250 -0
- package/src/projection/widget.ts +254 -0
- package/src/service.ts +276 -0
- package/src/session.ts +114 -0
- package/src/action-handlers.ts +0 -341
- package/src/agent-end-handler.ts +0 -304
- package/src/before-agent-start-handler.ts +0 -182
- package/src/budget.ts +0 -159
- package/src/command-handler.ts +0 -280
- package/src/state.ts +0 -250
- package/src/templates.ts +0 -232
- package/src/tool-handler.ts +0 -252
- package/src/widget.ts +0 -186
package/src/service.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service 协调层 — 命令/事件入口(applyEvent)
|
|
3
|
+
*
|
|
4
|
+
* D-21: 不合并为单一 applyCommand。命令/事件路径在触发方/返回值/并发模型上全不同。
|
|
5
|
+
* engine 层纯函数是真正共享层。
|
|
6
|
+
*
|
|
7
|
+
* D-16: service 不持有 ctx,通过 ports 参数接收能力。
|
|
8
|
+
*
|
|
9
|
+
* FR-3.1: createGoal 唯一创建入口(/goal set 与 __goalInit 都走它)
|
|
10
|
+
* FR-3.3: finalizeAndPersist 唯一终态序列入口(tick → finalizeGoal → persist)
|
|
11
|
+
* finalizeGoal 只做 transitionStatus + writeHistory(纯)
|
|
12
|
+
* FR-6.5: persist 前调 tick 累计时间
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
accumulateTokens,
|
|
17
|
+
checkBudgetOnResume,
|
|
18
|
+
checkBudgetOnTurnEnd,
|
|
19
|
+
tick,
|
|
20
|
+
} from "./engine/budget";
|
|
21
|
+
import { createGoalState, isActiveStatus, transitionStatus } from "./engine/goal";
|
|
22
|
+
import type { BudgetConfig, GoalRuntimeState, GoalStatus } from "./engine/types";
|
|
23
|
+
import { makeHistoryEntry, serializeState } from "./persistence";
|
|
24
|
+
import type {
|
|
25
|
+
GoalHistoryEntry,
|
|
26
|
+
MessagingPort,
|
|
27
|
+
PersistencePort,
|
|
28
|
+
SessionPort,
|
|
29
|
+
UiPort,
|
|
30
|
+
} from "./ports";
|
|
31
|
+
import { updateWidget } from "./projection/widget";
|
|
32
|
+
import type { GoalSession } from "./session";
|
|
33
|
+
|
|
34
|
+
// ── Ports 组合 ────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface ServicePorts {
|
|
37
|
+
persistence: PersistencePort;
|
|
38
|
+
ui: UiPort;
|
|
39
|
+
messaging: MessagingPort;
|
|
40
|
+
session: SessionPort;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Event 效果 ────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export type EventEffect =
|
|
46
|
+
| { kind: "sendContextMessage"; content: string; deliverAs: "steer" | "followUp"; customType?: string }
|
|
47
|
+
| { kind: "sendUserMessage"; content: string; deliverAs: "steer" | "followUp" }
|
|
48
|
+
| { kind: "notify"; text: string; level: "info" | "warning" | "error" }
|
|
49
|
+
| { kind: "clearSession" }
|
|
50
|
+
| { kind: "updateWidget" };
|
|
51
|
+
|
|
52
|
+
// ── 持久化辅助 ────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* FR-6.5 tick 核心:按当前 status 累加运行时间(active 才累加),mutate state。
|
|
56
|
+
*
|
|
57
|
+
* 用于 active→非活跃转换前捕获最后一段运行时间——调用方在 `transitionStatus`
|
|
58
|
+
* **之前**调本函数,使 tick 看到 active 状态并累加当前运行段。
|
|
59
|
+
*
|
|
60
|
+
* 导出供 `persistState`(service/command/tool 路径)、`persistAndUpdate`(event 路径)、
|
|
61
|
+
* 以及各 transition 调用点共用——**单一 tick 定义点**(BL-3 DRY)。
|
|
62
|
+
*/
|
|
63
|
+
export function tickState(state: GoalRuntimeState): void {
|
|
64
|
+
const isRunning = isActiveStatus(state.status);
|
|
65
|
+
const ticked = tick(state.timeStartedAt, state.timeUsedSeconds, Date.now(), isRunning);
|
|
66
|
+
state.timeUsedSeconds = ticked.timeUsedSeconds;
|
|
67
|
+
state.timeStartedAt = ticked.timeStartedAt;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* FR-6.5: persist 前调 tick 累加时间,然后 serialize + appendState。
|
|
72
|
+
*
|
|
73
|
+
* tick 使用**当前 status** 判断是否累加(active 才累加)。因此对于 clear/abort/blocked
|
|
74
|
+
* 这类「active → 非活跃」的转换,调用方必须在改 status **之前**先调本函数
|
|
75
|
+
* (或先调 {@link tickState} 捕获最后运行段)。
|
|
76
|
+
*
|
|
77
|
+
* 导出供 command-adapter / event-adapter 复用(DRY:所有路径共用同一持久化语义)。
|
|
78
|
+
*/
|
|
79
|
+
export function persistState(session: GoalSession, ports: ServicePorts): void {
|
|
80
|
+
if (!session.state) return;
|
|
81
|
+
tickState(session.state);
|
|
82
|
+
ports.persistence.appendState(serializeState(session.state));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 事件路径 persist + updateWidget(FR-6.5 tick + appendState + updateWidget)。
|
|
87
|
+
*
|
|
88
|
+
* 与 {@link persistState}(command/tool 路径)的差异:事件路径多 updateWidget + 可选 checkStale
|
|
89
|
+
* + **budget 终态检查(#5 单一检查点)**。两者都调 {@link tickState}(单一 tick 定义点,BL-3 DRY)。
|
|
90
|
+
*
|
|
91
|
+
* NFR F2:budget 终态检查在此函数内(事件路径单一检查点,对齐 Codex SQL CASE)。
|
|
92
|
+
* 不在 {@link persistState}(command/tool 路径)—— 否则 token 累加后检查永不触发。
|
|
93
|
+
* 仅 active 状态检查(paused/blocked/终态不重复触发;终态 goal 进不了此函数,event handler 入口已过滤)。
|
|
94
|
+
*
|
|
95
|
+
* @returns true 表示 state 已被新 goal 覆盖(checkStale 触发),调用方应中止后续副作用
|
|
96
|
+
*/
|
|
97
|
+
export function persistAndUpdate(
|
|
98
|
+
session: GoalSession,
|
|
99
|
+
ports: ServicePorts,
|
|
100
|
+
checkStale?: (() => boolean) | undefined,
|
|
101
|
+
): boolean {
|
|
102
|
+
if (!session.state) return false;
|
|
103
|
+
tickState(session.state);
|
|
104
|
+
|
|
105
|
+
// #5: budget 终态检查(事件路径单一检查点,NFR F2)。仅在 active 时检查,
|
|
106
|
+
// 避免对 paused/blocked/终态重复触发。checkBudgetOnTurnEnd 是 engine 纯函数,
|
|
107
|
+
// service 复用它不破坏纯 ports 设计(engine 是零 Pi 依赖纯函数层)。
|
|
108
|
+
if (session.state.status === "active") {
|
|
109
|
+
const budgetResult = checkBudgetOnTurnEnd(session.state, session.state.timeUsedSeconds);
|
|
110
|
+
if (budgetResult.terminal) {
|
|
111
|
+
const dim = budgetResult.terminal.dimension;
|
|
112
|
+
// FR-3.3: 唯一终态序列入口(finalizeAndPersist 内部 tickState 是 no-op——
|
|
113
|
+
// 上面已 tick,且状态此时仍 active——+ finalizeGoal + appendState)。
|
|
114
|
+
// terminal 分支不再单独 appendState:finalizeAndPersist 已含终态 state 持久化。
|
|
115
|
+
finalizeAndPersist(
|
|
116
|
+
session.state,
|
|
117
|
+
dim === "token" ? "budget_limited" : "time_limited",
|
|
118
|
+
0,
|
|
119
|
+
ports,
|
|
120
|
+
);
|
|
121
|
+
if (checkStale?.()) return true;
|
|
122
|
+
updateWidget(session, ports.ui);
|
|
123
|
+
return false; // 终态已处理
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 非终态路径:正常 persist + updateWidget
|
|
128
|
+
ports.persistence.appendState(serializeState(session.state));
|
|
129
|
+
if (checkStale?.()) return true;
|
|
130
|
+
updateWidget(session, ports.ui);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── FR-3.1 唯一创建入口 ──────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 唯一创建入口。两个调用源都走它:
|
|
138
|
+
* - goal_control create(toolcall,AI 提供 slug + objective)
|
|
139
|
+
* - __goalInit(index.ts,isExternalInit=true)
|
|
140
|
+
*
|
|
141
|
+
* 注:/goal <objective> 命令路径已改为提示词触发器——不直接调本函数,
|
|
142
|
+
* 而是 sendUserMessage 让 AI 调 goal_control create(slug 由 AI 生成)。
|
|
143
|
+
*
|
|
144
|
+
* isExternalInit=true 时不触发 sendUserMessage(__goalInit 不触发 AI)。
|
|
145
|
+
*
|
|
146
|
+
* @param slug AI 生成的短标识(optional,仅 widget 标题 + history 用)
|
|
147
|
+
* @returns true 如果创建成功,false 如果已有 active goal(拒绝创建)
|
|
148
|
+
*/
|
|
149
|
+
export function createGoal(
|
|
150
|
+
session: GoalSession,
|
|
151
|
+
objective: string,
|
|
152
|
+
budget: Partial<BudgetConfig>,
|
|
153
|
+
ports: ServicePorts,
|
|
154
|
+
isExternalInit: boolean,
|
|
155
|
+
slug?: string,
|
|
156
|
+
): boolean {
|
|
157
|
+
// 已有 active goal → 拒绝
|
|
158
|
+
if (session.state && isActiveStatus(session.state.status)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
void isExternalInit; // 保留参数位以备 future use(外部 init 的差异化行为)
|
|
163
|
+
session.state = createGoalState(objective, budget, slug);
|
|
164
|
+
|
|
165
|
+
persistState(session, ports);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── FR-3.3 唯一终态序列入口 ──────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 唯一终态序列入口(FR-3.3 / AC-3)。
|
|
173
|
+
*
|
|
174
|
+
* 收口所有 active→terminal 转换的完整副作用序列,消除此前散在 service /
|
|
175
|
+
* command-adapter / event-adapter 的重复 `transitionStatus + completedAtTurnIndex +
|
|
176
|
+
* writeHistory + appendState` 序列。
|
|
177
|
+
*
|
|
178
|
+
* 序列(严格顺序):
|
|
179
|
+
* 1. tickState(state)(FR-6.5:转 terminal 前累加当前运行段——此时 status 仍为 active)
|
|
180
|
+
* 2. finalizeGoal(state, terminalStatus, ports, { completedTasks })
|
|
181
|
+
* — transitionStatus(终态守卫) + completedAtTurnIndex= + appendHistory(FR-8.7 矩阵)
|
|
182
|
+
* 3. ports.persistence.appendState(serializeState(state))(持久化终态 state)
|
|
183
|
+
*
|
|
184
|
+
* @param state runtime state(mutate)
|
|
185
|
+
* @param terminalStatus 目标终态(complete / cancelled / budget_limited / time_limited)
|
|
186
|
+
* @param completedTasks 已完成任务数(写入 history entry)
|
|
187
|
+
* @param ports ServicePorts(persistence.appendHistory + appendState)
|
|
188
|
+
*/
|
|
189
|
+
export function finalizeAndPersist(
|
|
190
|
+
state: GoalRuntimeState,
|
|
191
|
+
terminalStatus: GoalStatus,
|
|
192
|
+
completedTasks: number,
|
|
193
|
+
ports: ServicePorts,
|
|
194
|
+
): void {
|
|
195
|
+
tickState(state);
|
|
196
|
+
finalizeGoal(state, terminalStatus, ports, { completedTasks });
|
|
197
|
+
ports.persistence.appendState(serializeState(state));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 终态变更 + 写 history(纯状态变更,不含 tick / persist / clearSession)。
|
|
202
|
+
*
|
|
203
|
+
* 被 {@link finalizeAndPersist} 内部调用,也可单独调用(如仅需状态变更 + history)。
|
|
204
|
+
* 按 FR-8.7 矩阵:所有终态都写 history(blocked 是中间态,不走此入口)。
|
|
205
|
+
*/
|
|
206
|
+
export function finalizeGoal(
|
|
207
|
+
state: GoalRuntimeState,
|
|
208
|
+
terminalStatus: GoalStatus,
|
|
209
|
+
ports: ServicePorts,
|
|
210
|
+
options: { completedTasks: number },
|
|
211
|
+
): void {
|
|
212
|
+
state.status = transitionStatus(state.status, terminalStatus);
|
|
213
|
+
state.completedAtTurnIndex = state.currentTurnIndex;
|
|
214
|
+
|
|
215
|
+
// FR-8.7: 所有终态都写 history(blocked 是中间态,不走此入口)
|
|
216
|
+
const entry: GoalHistoryEntry = makeHistoryEntry(state, options.completedTasks);
|
|
217
|
+
ports.persistence.appendHistory(entry);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 路径 B:applyEvent ────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 路径 B 入口。异步事件,返回 EventEffect[]。
|
|
224
|
+
* 并发保护(isProcessing / stale-check)在 event-adapter,不在此层。
|
|
225
|
+
*
|
|
226
|
+
* 本函数作为简单事件的统一入口(message_end / turn_end / agent_start)。
|
|
227
|
+
* 复杂事件(before_agent_start / agent_end / session_start)由 event-adapter
|
|
228
|
+
* 直接实现,调 engine 纯函数 + service 辅助函数。
|
|
229
|
+
*/
|
|
230
|
+
export function applyEvent(
|
|
231
|
+
session: GoalSession,
|
|
232
|
+
eventType: string,
|
|
233
|
+
eventData: unknown,
|
|
234
|
+
// TS-2: 参数未使用,放宽为 undefined 以消除调用方 `undefined as never` 断言。
|
|
235
|
+
// 保留参数位以备未来 event 类型需要 ports(applyEvent 是统一事件入口)。
|
|
236
|
+
_ports?: ServicePorts,
|
|
237
|
+
): EventEffect[] {
|
|
238
|
+
const effects: EventEffect[] = [];
|
|
239
|
+
if (!session.state) return effects;
|
|
240
|
+
|
|
241
|
+
switch (eventType) {
|
|
242
|
+
case "message_end": {
|
|
243
|
+
// token 累加(FR-8.6 G-R2-001)—— 仅 active 时累加(回归修复:原缺 isActiveStatus 守卫,
|
|
244
|
+
// blocked 等 non-active 状态会错误累加 token)。复用 engine 纯函数。
|
|
245
|
+
if (!isActiveStatus(session.state.status)) break;
|
|
246
|
+
const data = eventData as {
|
|
247
|
+
message?: {
|
|
248
|
+
role?: string;
|
|
249
|
+
usage?: { input?: number; output?: number; cacheRead?: number; totalTokens?: number };
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
if (data?.message?.role !== "assistant") break;
|
|
253
|
+
const usage = data.message.usage;
|
|
254
|
+
if (!usage) break;
|
|
255
|
+
session.state.tokensUsed = accumulateTokens(session.state.tokensUsed, usage);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "turn_end":
|
|
259
|
+
session.state.currentTurnIndex++;
|
|
260
|
+
effects.push({ kind: "updateWidget" });
|
|
261
|
+
break;
|
|
262
|
+
case "agent_start":
|
|
263
|
+
// task 已移除,agent_start 暂无副作用(#7 注入 todo 进度后可能重填)
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return effects;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── resume 预算重检(供 command-adapter 调用)─────────
|
|
271
|
+
|
|
272
|
+
export function checkResumeBudget(
|
|
273
|
+
state: GoalRuntimeState,
|
|
274
|
+
): { type: "exceeded"; dimension: "token" | "time" } | null {
|
|
275
|
+
return checkBudgetOnResume(state);
|
|
276
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 层 — 运行时句柄 + 状态重建 + entry GC
|
|
3
|
+
*
|
|
4
|
+
* GoalSession 是进程内瞬态句柄(不持久化)。
|
|
5
|
+
* reconstructGoalState 从 entry 恢复状态(session_start 时调)。
|
|
6
|
+
*
|
|
7
|
+
* FR-6.4: 删除 hasPendingInjection(僵尸字段)
|
|
8
|
+
* FR-6.7: 删除 pendingPause(ESC 改用 aborted 守卫)
|
|
9
|
+
* FR-8.1 G-006: session append-only——entry GC 不生效(生产),history 显示侧截断
|
|
10
|
+
* FR-3: 崩溃后保持原状态(active 重启计时;paused/blocked 保持;终态保持)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { GoalRuntimeState } from "./engine/types";
|
|
14
|
+
import { deserializeState, ENTRY_TYPE } from "./persistence";
|
|
15
|
+
import type { SessionEntryLike, SessionPort, UiPort } from "./ports";
|
|
16
|
+
|
|
17
|
+
// ── 运行时句柄 ────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface GoalSession {
|
|
20
|
+
state: GoalRuntimeState | null;
|
|
21
|
+
/** 防重入标志:agent_end / before_agent_start 等事件处理器入口检查 */
|
|
22
|
+
isProcessing: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createGoalSession(): GoalSession {
|
|
26
|
+
return {
|
|
27
|
+
state: null,
|
|
28
|
+
isProcessing: false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Stale Context 检测(FR-8.2 G-010)─────────────────
|
|
33
|
+
|
|
34
|
+
export const STALE_CONTEXT_PATTERNS = [
|
|
35
|
+
"aborted",
|
|
36
|
+
"context canceled",
|
|
37
|
+
"stale context",
|
|
38
|
+
"stalecontext",
|
|
39
|
+
"extension context no longer active",
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export function isStaleContextError(error: Error | unknown): boolean {
|
|
43
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
44
|
+
const lower = msg.toLowerCase();
|
|
45
|
+
return STALE_CONTEXT_PATTERNS.some((p) => lower.includes(p));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── reconstructGoalState(session_start 时调)──────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 从 session entries 恢复 goal state。
|
|
52
|
+
*
|
|
53
|
+
* Pi SDK 的 session 是 append-only,getEntries() 返回 filter-copy——splice
|
|
54
|
+
* 无法修改真实 entries。Entry GC(goal-state 留 1、goal-history 留 20)在生产
|
|
55
|
+
* 不生效:本函数只读最新一条 goal-state,不删旧 entry;history 显示侧用
|
|
56
|
+
* .slice(-MAX_HISTORY_ENTRIES) 截断(handleHistory)。长期需 compaction API。
|
|
57
|
+
* FR-3: 崩溃后状态保持原状——active 重启计时(timeStartedAt = now),
|
|
58
|
+
* paused/blocked 保持(用户/agent 主动叫停不被抹除),终态保持。
|
|
59
|
+
* FR-8.1 G-024: deserialize throw → state=null(部分损坏全丢)
|
|
60
|
+
*/
|
|
61
|
+
export function reconstructGoalState(session: GoalSession, sessionPort: SessionPort): void {
|
|
62
|
+
session.state = null;
|
|
63
|
+
const entries = sessionPort.getEntries();
|
|
64
|
+
|
|
65
|
+
// 找到最新的 goal-state entry(从后往前)
|
|
66
|
+
let latestStateIdx = -1;
|
|
67
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
68
|
+
if (isGoalStateEntry(entries[i]!)) {
|
|
69
|
+
latestStateIdx = i;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (latestStateIdx >= 0) {
|
|
75
|
+
const data = entries[latestStateIdx]!.data as Record<string, unknown> | undefined;
|
|
76
|
+
if (data) {
|
|
77
|
+
try {
|
|
78
|
+
session.state = deserializeState(data);
|
|
79
|
+
} catch {
|
|
80
|
+
// FR-8.1 G-024: 部分损坏全丢
|
|
81
|
+
session.state = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// FR-8.1 G-006: session append-only,splice GC 在生产不生效(见上方函数注释)。
|
|
87
|
+
// 不在此处删旧 entry;history 显示侧截断。
|
|
88
|
+
|
|
89
|
+
if (!session.state) return;
|
|
90
|
+
|
|
91
|
+
// FR-3: 崩溃后状态保持。
|
|
92
|
+
// - active:重启计时(timeStartedAt = now,开启新运行段)
|
|
93
|
+
// - paused/blocked:保持原状(对称设计——用户/agent 主动叫停的状态不被崩溃抹除)
|
|
94
|
+
// - 终态:保持终态(不会被强制激活)
|
|
95
|
+
if (session.state.status === "active") {
|
|
96
|
+
session.state.timeStartedAt = Date.now();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isGoalStateEntry(entry: SessionEntryLike): boolean {
|
|
101
|
+
return entry.type === "custom" && entry.customType === ENTRY_TYPE;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── clearGoalSession ──────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export function clearGoalSession(session: GoalSession, uiPort: UiPort): void {
|
|
107
|
+
session.state = null;
|
|
108
|
+
session.isProcessing = false;
|
|
109
|
+
// FR-6.6: hasUI 守卫
|
|
110
|
+
if (uiPort.hasUI) {
|
|
111
|
+
uiPort.setWidget("goal", undefined);
|
|
112
|
+
uiPort.setStatus("goal", undefined);
|
|
113
|
+
}
|
|
114
|
+
}
|