Repository navigation
llm/retry 在链状态缺失时写出重复的 retry=1 + 全新 retryId,导致会话日志被校验器判为损坏且无法打开 #9163
Replies: 6 comments 2 replies
|
Your reading of The writer's chain identity is not the validator'sWriter, // line 79
function retryStateKey(provider: string, policyKey: string): string {
return JSON.stringify([provider, policyKey]) // two components
}
// line 131, the unit's own fold
apply: (state, event) => {
if (event.type === 'step/start' || event.type === 'turn/end') return {} // the ONLY reset
...
}Validator, const prior = this.retries.findLast(item =>
['turn', 'step', 'provider', 'policyKey'].every(key => item[key] === data[key])) // four components
if (attempt !== (prior === undefined ? 1 : prior['retry'] + 1))
throw new SessionFormatError('llm/retry skips its policy attempt sequence')
if (prior === undefined ? this.retries.some(item => item['retryId'] === id) : prior['retryId'] !== id)
throw new SessionFormatError('llm/retry must keep one retryId per policy chain')The unit's doc comment states the two-component view as intended — "Retry state for the current step by provider and policy" (
That asymmetry is also why "twenty-odd sessions had normally incrementing chains and passed": there, both notions agreed. One more thing the state cannot tell youLine 135: Where the "two Session objects" candidate sitsYour third hypothesis is the one the projection cannot rule out from inside. private readonly registrations = new Map<string, Registration>()
// Registration.cells: Map<Session, UnitCell> — keyed by the INSTANCE
private cellFor(registration, session) {
let cell = registration.cells.get(session)
if (cell === undefined)
cell = this.buildCell(registration.def, session.header, session.inheritedEventCount,
session.snapshotEvents()) // folds THIS object's logSo What is reachable from a plugin, and what is not
Your item 2 has a read-side analogue that is reachable, and it is the one this class of bug needs most, because the corruption is only ever discovered at the next open. We already ship that shape: If it would be useful here, the same live observer can carry this invariant — four-tuple chain, |
|
Agreed on the key-shape point, and thanks for the counter-check — it removes my earlier hypothesis for this sample: with one provider and one policyKey, the writer's reset-only-on- Tracked: I'll extend |
|
补一条和"已经写坏的会话"有关的:这条闸门拒读的已有工件可以离线修回来,不用等写入侧修好。 先复现你的形状:在一份真实的 4375 事件 v4 会话里,把同一条链(
修法:折叠的规则本身就把两个值钉死了,所以都不是猜的 ——
实测:上面那份修完, 我们 refuse 的情形(不猜):链身份含对象、 边界:这只修已经落盘的工件,不碰写入侧 —— 你定位的 安装/更新:桌面端「插件」页 → dsh-session-surgeon → 更新; |
|
@maozhuoshushu — thanks, the independent re-implementation is exactly the check I wanted, and a 126-log store is a far wider corpus than anything I can run here. On point 3, one measurement that changes the picture: the published package does not contain That said, the two credential-shaped hits were ours to remove and now are: the fake The rest I cannot remove: One thing while you have the corpus. This release adds two report-only codes from the same walker — |
|
把那两条码的实测口径贴一下,供后来者横向对照。我们这边独立复刻了二者(本地安装里码名还不存在,判定逻辑在 两个可能对别人有用的坑:
顺带更新原帖缺陷状态:39 份现役日志里 |
|
@maozhuoshushu 先回答你那个问题:写侧没修,原帖不该按已修关掉。 三版逐字节相同我把
const previous = ctx.sessionProjections.stateOf(agent.session, "llmRetry")[retryStateKey(provider, policyKey)];
const previousRetry = previous?.retry ?? 0;
const retry = previousRetry + 1;
const retryId = previous?.retryId ?? RetryId(randomUUID());
为什么会空:两个读端的链身份不是同一个东西
apply: (state, event) => {
if (event.type === "step/start" || event.type === "turn/end") return {}; // 清空整张表
if (event.type !== "llm/retry") return state;
const key = retryStateKey(event.data.provider, event.data.policyKey);
...
}两者等价的前提是重置点和折叠的 step 边界对齐。你现场那个 step 区间里没有任何 这解释了为什么它低频但不可逆:触发条件是「投影状态与日志不一致」,而不是「发生过重试」。要收口得让读侧不再依赖可重建的投影状态,或者把 所以原帖建议留着:它不是已修,是潜伏。 你提的
|
Uh oh!
There was an error while loading. Please reload this page.
Summary
@deepseek-ai/dsh-llm-retry在链状态取不到时,会把同一条 policy 链上的多次重试全部写成retry: 1,并且每次都用一个新的retryId。这样写出的会话日志会被@deepseek-ai/dsh-session-persistence-jsonl的官方校验器判为损坏:后果是整个会话无法打开(
stored session "..." is corrupt)。而且这是静默故障:损坏的日志会继续正常追加写入,直到该会话被重新打开时才暴露。Reproduction
无法稳定复现,属低频故障。一组样本共 119 个会话,其中 1 例出现(其余 118 例通过校验);同批另有二十余个会话存在正常递增的多重重试链(
retry1..N),所以不是「只要重试就坏」。出问题的那一份现场特征:
llm/retry27 条 +llm/retry-started27 条),分属 9 条 policy 链(链身份 =turn/step/provider/policyKey四元组)。llm/retry全部为retry: 1,且 27 个retryId两两不同。按官方链规则复算:18 条违反retry === prior.retry + 1,8 条违反「一条链只用一个retryId」。step/start或turn/end事件。model/selection的 provider 切换。Current behavior
dsh-llm-retry/lib/index.js的recover():当
stateOf()对这条链返回空表时,previousRetry退化为 0,于是每次重试都写出retry: 1+ 全新 UUID,与「一条链一个retryId、retry递增」的既定语义自相矛盾。插件本身不报错,事件照常落盘,问题被推迟到「下次打开该会话」。已排除(仅就本样本而言):
apply侧被step/start/turn/end清零——该 step 区间内这两个事件都不存在;dsh-session-projection/lib/index.js:296在行不可用且baseSeq > 0时直接抛错,否则整表 refold。因此嫌疑落在运行时状态:同一会话存在两个 Session 对象、projection cell 被替换、或 eager drive 未触达该 unit。这一环尚未定住。
Expected behavior
以下任选其一即可显著改善:
stateOf()取不到链状态时应视为不变量被破坏,而不是当作「首次重试」。至少落一条诊断事件,或直接放弃本次重试,避免写出会被自家校验器拒收的日志。llm/retry在 append 时若能发现自己违反链规则,应在写入前报错/给出明确诊断,而不是留下一个「沉默损坏、重开才炸」的会话。retry递增 + 一条链一个retryId)应有共同定义,避免「写端不知道自己在违规」。Environment
@deepseek-ai/dsh:0.1.7-alpha.2(Windows 本地安装 / 桌面版)@deepseek-ai/dsh-llm-retry:0.1.7-alpha.2@deepseek-ai/dsh-session-persistence-jsonl: 随0.1.7-alpha.2安装session.v4.jsonl.zstd);该样本 5136 帧 / 9229 事件retryPolicy为默认值(mode: "normal",maxRetries: 5)附:样本的修复方式(只改日志,未改写端)
对已损坏的那一份日志做了等长字符串改写(不重编码 JSON):把违规的 18 条
retry重编号为链内递增序号,并把 27 个retryId归并到链首 id。修复后官方校验器原地 PASS(events=9229),会话可正常打开。这只是抢救了那一份文件,写端缺陷仍在,所以提这个报告。若维护者需要,可以进一步提供该样本的结构化事件序列(脱敏后)。
All reactions