Skip to content

feat(memory): 基于模型判断 L1 提取并复用同 Session 历史 Episode - #681

Open
SeeSeng wants to merge 27 commits into
MemTensor:v1.2.6from
SeeSeng:l1-turn-judge-1019
Open

SeeSeng wants to merge 27 commits into
MemTensor:v1.2.6from
SeeSeng:l1-turn-judge-1019

Conversation

@SeeSeng

@SeeSeng SeeSeng commented Oct 20, 2026 •

Copy link
Copy Markdown

修改内容

L1 默认启用 turnJudge,保留联合判断中的任务关系、反馈及 L1/User Memory 判断,不改变模型输入输出字段。Episode 匹配只在当前 Session 内复用历史任务,支持 ABA/ABAB 任务切换。

先保存,再后台处理

  • 捕获入口先在本地事务中持久化原始轮次或事件及后台任务,固定 Session、轮次和来源身份,不等待摘要或 Episode 模型。完成响应允许没有 episodeId,并返回 assignmentStatus=pending。
  • L1 判断与 Episode 选择通过现有 worker 并行执行、分别保存结果;L1 可通过 raw_turn_id 独立保存。最终归属等待所需结果齐备后在本地事务中提交,不新增空 Episode。
  • 同 Session 按稳定轮次顺序处理;首次归属补齐 L1、来源、反馈、工具记录及 Skill trial 等关联。已关闭 Session 的已接受记录仍可恢复处理,并保持关闭状态。
  • 匹配失败保留原文及已生成 L1,只重试未完成部分。手动重试和启动恢复保留原始判断模式及轮次标志。
  • 待归属 L1 不提前奖励或反思;正反馈演化延后到归属完成,避免空 Episode 分组及缺失来源的 L2。

移除 capture_replay

  • 删除请求参数快照重放器、公开捕获接口重放及专属 worker 分支,新后台任务仅引用已保存记录的 ID。
  • 仅保留旧 capture_replay 数据的一次性转换与脱敏导入保护;旧记录转换后删除,不再创建或执行此类任务。
  • SQLite v10 和 PolarDB runtime-v5 允许原始轮次及 Skill trial 的 Episode 关联暂时为空;同步更新 Memory/Backend 契约及待归属读取流程。

验证结果(2026-10-27,本次提交)

  • Memory 全量:1,762 项通过,1 项平台相关跳过,0 失败。
  • Memory TypeScript 检查通过。
  • 完整 npm run smoke:memory-layer 通过:Backend 及依赖构建、8 项 smoke 测试、smoke 类型检查,以及 turnJudge=false/true 各 12 项 Backend→Memory HTTP 端到端检查。
  • 两种模式分别覆盖接收时原文落库、异步完成状态、L1/Episode 关联、索引及召回;HTTP 故障测试分别覆盖 Episode 选择失败和 L1 判断失败后的恢复及重复提交。
  • 专项覆盖两项模型调用分别阻塞、L1 成功而匹配失败、关闭和重启恢复、原判断输入保持、反馈延后处理、跨 Session 隔离、无空 Episode、旧数据迁移及导出脱敏。
  • HTTP 故障测试已接入正式 smoke 命令。使用持久化处理状态判断完成,不使用固定等待作为成功依据。

验证范围

本轮为 macOS 本地隔离 SQLite、真实 HTTP 服务和可控模型替身测试,未调用外部模型;未重新执行安装包/安装验证,未进行在线 PolarDB 集成验收。此前的打包及全 workspace 验收不作为本次提交的验证结果。

本分支已通过 merge 合入 MemTensor/memmy-agent 的 v1.2.6;本次修改继续保留默认开启及显式关闭 turnJudge 两条测试路径。

Anson-Se and others added 18 commits October 14, 2026 13:18
Add algorithm.session.turnJudge (default false) to gate the per-turn
judgment (turn.judge.v1). Let a summary-role call pass inputTokenBudget
to replace the 8K-derived input budget for that call only, and export
the cl100k counter and middle-cut helper so callers can fit their input
the same way the client does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move CAPTURE_TITLE_MAX_CHARS and CAPTURE_SUMMARY_MAX_CHARS into
capture-summary.ts (span-pipeline re-exports them) so the per-turn
judgment module can use them without an import cycle, and raise the
summary limit from 180 to 300. Import summaries share the constant and
move to 300 with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turn-judge.ts holds the per-turn prompt as finalised in
L1逐轮整合提示词.md section 1, the TASK SO FAR / THIS TURN assembly with
head/tail cuts and the fixed cut order within a 12800-token budget,
quote alignment (as written, ignoring whitespace/punctuation/width/case,
split at ellipses), and a pure parser whose reject/record split follows
the offline check.v17. turn-judge-context.ts builds TASK SO FAR from the
stored turns before this one via the new completedRawTurnsBefore query.
Host-generated turns are recognised by fixed opening tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With algorithm.session.turnJudge on, the capture job carries turnJudge
and promptVersion and makes one turn.judge.v1 call instead of the
capture-only decision. The input is built from the stored turn (USER in
full, TASK SO FAR cut off at this turn), stored on the raw turn and
reused by every retry. Relation, feedback and reason are recorded on the
raw turn and in capture_decision (prompt_version, unverified_answer,
dropped quotes) but routing is unchanged until PR-2.

First-layer capture changes under the switch: no real-time keyword
guard; forced L1 only for the device memory observation; an L1 is
provisional only when the model says unverified_answer and the turn has
no non-recall tool result; new L1 is no longer marked provisional for
looking like a question. Earlier turns captured late along with this
one are judged with TASK SO FAR empty. Test doubles return the five keys
when they see the new prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dback

Downstream reads for per-turn feedback, where most reactions are neutral
(strength 0):
- reward fallback and the reward job's feedback payload pick the latest
  reaction with an attitude (representativeEpisodeFeedback), so
  negative->neutral and positive->neutral no longer score 0;
- the L3 World Model does not take a neutral reaction as evidence;
- a model-judged negative (turn_judge) gets a decision repair only from a
  model draft, like the old revision feedback;
- the episode detail and service metrics count positive, negative and
  neutral feedback separately.
No old record has a zero-strength neutral reaction, so old data reads the
same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With algorithm.session.turnJudge on (merge mode):
- the start of a turn calls no model; a completed turn is filed
  provisionally in the open task (or a new one), and the explicit
  end-topic rule is left to the model;
- per-turn judgments of a session run in turn order (lease condition on
  turnAt), so each is built on the settled result of the one before;
- follow_up files the turn in the judged task (reopening a closed one
  inside the merge window), new_task closes it and starts a new one with
  L3 frozen through the old task only, end_topic closes it; the turn
  moves with its L1, feedback, repairs, Skill trials, L3 input, source
  ledger and unfinished jobs in one transaction (moveTurnLinks), and the
  closing work of every touched task runs after the commit; a turn
  delivered after later turns were judged is not moved;
- every turn writes one explicit reaction (positive 1, negative 1,
  neutral 0) on the task holding the previous turn, pointing at that
  turn and its L1, deduplicated per judged turn; repairs only for
  negative, Skill trial settlement only for positive and negative;
- host-generated turns are recognised by tag and skip the model;
- a judgment that fails every attempt marks its capture record failed so
  the task can close; manual retry, startup repair and the summary guard
  rebuild the job with the judgment flags and reuse the stored input;
- relation confidence becomes optional and the relation record carries
  ["llm", "turn.judge.v1"].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the session's previous completed turn sits in a closed task, the
per-turn judgment leaves TASK SO FAR empty (filedByRule
previous_task_closed), as for the first turn of a session, matching the
offline evaluation's state machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A turn whose previous task is closed is filed by rule as a new task: it
stays in its new provisional task, or moves out if it waited in the task
that closed (e.g. after an end_topic). The judgment no longer reopens a
closed task inside the merge window; that branch is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… once judged

The stored input of a turn.judge.v1 call leaves out the system prompt,
which is fixed for the prompt version and put back from code when a
retry reads it. After a successful judgment only the small fields stay
(base task, previous turn, token count, cut steps), unless
algorithm.session.turnJudgeKeepInput is on (for checking what the model
saw during testing). Failed and pending judgments keep the full input
for retries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	Memory/src/service/embedding/embedding-job-processor.ts
#	Memory/src/service/memory-service.ts
Sync with L1逐轮整合提示词.md section 4 items 20 and 21:
- recommendations, suggestions, plans, drafts and wording the assistant
  produced are its work, not unverified facts;
- choosing one of the options the previous reply offered creates L1
  (kind decision), shown by a new Part 3 example;
- drop repeated text: "kind=correction" in the first Part 3 case (the
  evidence kinds define it), "(may be empty)" on l1.evidence (Rules for
  values says it), and the forced-L1 message's last sentence about user
  (Output requires all five keys).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejected (soft-deleted) L1 kept the episode it was provisionally filed
in after its turn moved. moveTurnLinks now rewrites episode_id and the
trace key of the turn's soft-deleted L1 rows directly; live rows are
still rewritten through the memory repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync with L1逐轮整合提示词.md section 4 item 22. In offline runs
qwen3.8-27b judged follow-up questions, discussion and "keep these for
later" as positive, reasoning that the message stays in the task and
does not fault the result:
- Part 2 says that staying in the task (Part 1) shows no attitude;
- positive: a question about the result or another part of the task, a
  discussion of it, or setting it aside is neutral; inferred approval is
  still a next action that uses or builds on the result;
- neutral lists the same three cases;
- two neutral examples: asking how the paper's experiments were designed
  after its method was explained, and keeping review suggestions for later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select historical episodes before creating tasks, using user-only fallback when summaries are missing or incomplete. Keep model calls outside transactions and adapt async entry points and tests. Include isolated organization app packaging.
@Anson-Se

Copy link
Copy Markdown
Contributor

@origami-chen

Anson-Se and others added 3 commits October 22, 2026 05:34
Part 4 recorded whole task requests, one-off changes, project status and
design opinions as user preferences. A preference now has to be stated as
a default for future conversations or as a lasting habit; requests and
constraints of the current task, project status, conventions tied to a
named project, and views on how a system should be designed stay in L1.
Facts about the user mentioned in passing inside a request are still
recorded. A convention tied to a named project creates only L1.

The output format is unchanged, so the prompt version stays turn.judge.v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
if (this.deps.shouldDeferEpisodeSelection?.()) {
throw new MemoryServiceError("conflict", "episode selection is waiting for the summary model token budget");
}
results.set(error.key, await selectEpisode(this.deps.llm, error.input));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

摘要模型不可用时,新任务的原始轮次也无法保存

已配置摘要模型,但模型请求失败或额度暂时耗尽时,首次捕获某个 Session 的轮次、或其他需要新建 Episode(任务分组)的提交会整体失败,原始对话和后续处理任务都没有落库;默认 turnJudge=false 也受影响。补充测试中,同一模型故障下 base 保存了原始轮次,head 没有保存。这不代表外部聊天记录被删除,但 Memory 无法从自己的队列恢复该提交;只能依赖调用方重试或之后重新扫描。

原因是 completeTurn/completeSourceTurn 把整个写入放入 withEpisodeSelection 的事务:ensureEpisode 要求先调用模型,事务先回滚,随后第 525 行等待选择结果;模型异常直接向上传播。实时 DeepSeek hook 已清理本轮待捕获状态,失败后只记日志,因此这里不能依赖所有调用方都会重试。

建议在统一捕获入口先持久化已完成轮次或待处理提交,再异步选择/复用 Episode;模型失败应保留可恢复的待办,恢复后幂等完成归类,同时继续保证不生成空 Episode。覆盖 source hook/扫描、直接完成轮次及共用此入口的工具观察、压缩和子任务记录,验证模型断网/额度不足后原文仍可恢复、重试不重复,并保留同 Session 复用和跨 Session 隔离。

relationRouting: { action }
}, at);
}
if (decision.relation === "new_task" && session.meta.l3_world_model_protocol_version === 2) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

切换任务时,当前轮对上一任务的反馈没有进入 L3 更新

新版本算法逻辑,Session 使用 L3 v2 时,用户先评价上一任务“这个结果不对”,再切换到新任务;如果模型同时给出 new_task 和负面反馈,反馈虽然写入数据库,却没有进入此次 L3 长期记忆更新使用的证据。补充测试实际得到上一轮原文已在批次中、反馈编号列表却为空;相同场景在 base 会包含该反馈。

原因是 applyTurnJudgment 先调用 routeJudgedTurn,后调用 recordTurnJudgeFeedback。前者在这里执行 freezeBatches,提前固定了旧任务的原文和反馈编号,并记录这些原文已安排处理。后续 L3 只读取批次保存的反馈编号;随后写入的反馈不会自动加入这个批次,也不会因下一批处理新轮次而补回。因此问题是证据遗漏,反馈记录本身仍然存在。

建议将顺序统一为完成轮次归类及关联迁移、保存当前判断产生的反馈,再固定 L3 证据并排队。回归应分别覆盖正面和负面反馈伴随 new_task、积压轮次和历史 Episode 重新打开,检查实际提交给 L3 的证据包含对应反馈,且后续批次不会遗漏或重复它。

Comment thread Memory/src/storage/repositories.ts Outdated
AND json_extract(earlier_turn_job.payload_json, '$.turnJudge') = 1
AND earlier_turn_job.status IN ('queued', 'leased', 'failed')
AND earlier_turn_job.id <> evolution_jobs.id
AND CAST(json_extract(earlier_turn_job.payload_json, '$.turnAt') AS TEXT)

@origami-chen origami-chen Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

两轮时间戳相同时,并发判断会把新任务留在旧分组

同一个 Session 的两轮如果用于排序的时间戳相同、都在等待 worker 判断,且后一轮的记忆判断先返回,先保存的轮次即使被模型明确判断为新任务,也可能留在旧 Episode(任务分组)。外部 Agent 正常串行对话通常保留不同的原生开始时间,因此一般不易触发;更应关注 Memory 轮次完成接口 POST /api/v1/turns/:turnId/complete 的密集或高并发提交。该接口在此前没有建立原始轮次时使用服务处理时间,可能产生相同毫秒的记录。并发本身并不充分,快速顺序提交也可能碰到同一时间戳。

队列实际比较 rawTurn.createdAt:原生来源提交使用 startedAt,直接完成接口通常使用记录建立时间,并非要求两个聊天模型同时返回。领取条件只比较 turnAt <,没有区分时间相同的轮次,所以 worker 可以并发判断它们。后一轮先完成后,前一轮命中 later_turn_already_judged,忽略自己的 new_task 归类。补充测试通过直接完成接口固定写入时间并控制判断顺序:先处理 A,再写入同时间的 B、C,让 C 判断先完成,最终 A、B 在同一分组,三个独立任务只得到两个 Episode。这证明边界存在,不代表普通串行聊天经常发生。

建议让同一 Session 的队列与上下文查询使用一致、稳定的轮次顺序;时间相同时也按持久化序号或一致的插入次序,确保前一轮判断完成后才能领取后一轮。回归覆盖密集接口提交的同时间轮次、模型逆序返回、失败重试/恢复,检查 Episode 归属和关联 L1/反馈,并保留原生来源时间不同的正常串行路径及真正迟到历史轮次的处理规则。

}

completeSourceTurn(request: SourceTurnCompleteRequest): SourceTurnCompleteResponse {
async completeSourceTurn(request: SourceTurnCompleteRequest): Promise<SourceTurnCompleteResponse> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

异步接口改动遗漏了 OpenCode 验证脚本的两个调用

completeSourceTurn 改为返回 Promise 后,scripts/test-opencode-review.mjs:255 仍立即读取 result.status,第 151 行也把 Promise 放入 scanFirstResults,第 81 行随后按普通响应读取状态。脚本执行到这些步骤时会把正常捕获/去重判为失败,无法完成原有的 hook-first 和 scan-first 验证。

补充测试从该脚本直接提取两处调用,配合实际 MemoryService 和临时 SQLite 执行:base 分别读到 existing、stored,head 两处均为 undefined。这验证了本地脚本的接口不兼容;官方 OpenCode V2 全链路因指定二进制不可获得而尚未执行。

建议给这两处调用补齐 await,使列表保存已完成响应,并保证 scan-first 确认写入后再转发 hook 请求;随后使用准确的官方 V2 二进制重跑两种先后顺序,验证只保存一次且状态断言正确。

Comment thread Memory/src/config/index.ts Outdated
followUpMode: "merge_follow_ups",
mergeMaxGapMs: 2 * 60 * 60 * 1000
mergeMaxGapMs: 2 * 60 * 60 * 1000,
turnJudge: false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个应该默认true?现在这样新算法逻辑发版后默认不生效吧?

Xucheng and others added 3 commits October 26, 2026 12:51
Journal captures before external Episode selection so model failures retain
original requests for worker recovery. Commit capture and journal completion
atomically, protect live leases during startup, and wake automatic retries.

Save new-task feedback before freezing L3 evidence. Serialize equal-time
judgments using RawTurn insertion order, including existing queued payloads.
Await both asynchronous source captures in the OpenCode review harness.
Enable turnJudge by default and explicitly opt legacy fixtures out.

Validation: Memory typecheck; 1740 Memory tests passed (one Windows-only skip);
62 AgentSourceCore tests; 24 OpenCode compatibility tests. Isolated live
configured gpt-6-luna calls verified first-turn, follow-up/negative, and
new-task/positive judgments with persisted routing and feedback. The live
smoke uses synthetic dialogue and mock embeddings, without temperature
adaptation. Official OpenCode 2.0.26 host E2E remains unavailable locally.
turnJudge is now on by default, which made Episode creation require model
selection even when no summary model is configured: every first turn of a
Session or new task was refused (409) and only queued for replay. Fall back
to the existing offline capture mode instead, as before the default changed.

A journaled source capture replayed after a later turn of the same
conversation was stored returned pending/source_turn_out_of_order inside the
replay commit, so the job was marked succeeded with its payload stripped and
the turn was lost. During replay, keep such turns as detached history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge turn-judge-capture-fixes into l1-turn-judge-1019 with both accepted
behaviors: capture offline when the summary model is unconfigured, and
preserve replayed out-of-order source turns as detached history.

Validation: Memory typecheck passed; 1742 tests passed with one
Windows-specific test skipped. turnJudge remains enabled by default.
if (this.deps.shouldDeferEpisodeSelection?.()) {
throw new MemoryServiceError("conflict", "episode selection is waiting for the summary model token budget");
}
results.set(error.key, await selectEpisode(this.deps.llm, error.input));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Episode 匹配阻塞轮次落库,应改为先保存、后台归属

问题是什么: PR 将“模型匹配历史 Episode”放到了原始轮次保存之前,使保存对话依赖模型成功。base 在相同故障场景中先保存轮次、再后台生成摘要,不存在这项回归。“先匹配历史 Episode,再决定是否新建”应约束 Episode 的创建顺序,不应阻塞原始轮次或 L1 的保存。

已经修了什么: 本轮新增 capture_replay,在调用模型前保存原始操作和参数,已经解决常规模型故障下缺少恢复依据的问题,并支持重启恢复和重复执行保护;completeSourceTurn 的迟到历史处理也已适配。

还有什么没修: 输入仍暂存在接口重放任务中,原始轮次的正式落库仍依赖 Episode 选择。重放再次调用公开接口,因此关闭 Session 后会被 requireOpenSession 拦截。

应该怎么修: 将流程明确改为以下五步,复用现有 worker 和重试机制:

  1. 接收并持久化。 请求阶段只做校验和本地保存,不等待摘要或 Episode 匹配模型。在同一事务中保存原始轮次或事件及后台任务,固定 Session、轮次和来源身份;允许原始轮次的 episode_id 为空,并明确标记“待归属”。接口返回“已保存、待处理”。
  2. 后台生成 L1。 依据模型结果决定是否生成 L1;需要生成时,使用 raw_turn_id 关联原始轮次,允许 L1 暂时没有 episode_id。已生成的 L1 独立保存,不能因后续匹配失败而回滚或重复生成。保留联合判断所需的前序上下文,同 Session 按稳定顺序处理,不能把前序轮次待归属误判为没有历史。
  3. 后台确定 Episode。 匹配同 Session 的历史 Episode;明确匹配则复用,没有匹配才新建。新建 Episode 与首次绑定轮次一起提交,避免空 Episode;同时补齐该轮 L1、来源记录及相关任务等关联。即使模型决定不生成 L1,原始轮次也要完成归属。
  4. 归属完成后处理依赖项。 确定相关反馈的归属,再执行依赖 Episode 的汇总、评估及记忆演化。匹配失败只重试尚未完成的阶段;Session 关闭后,已接受的记录继续处理,关闭状态保持不变。
  5. 移除 capture_replay。 删除原始接口参数的持久化重放、公开接口再次调用及其专属任务分支。后台直接根据已保存的记录继续处理。若已有旧待办,先将其转换为原始记录和后台任务,再移除旧机制。

移除后的导出与契约也要一起验收:后台任务引用已保存记录的 ID,不再携带完整接口请求。includeRawText=false 导入后不得通过后台任务或旧待办转换重新填回已排除的 raw_turns 原文字段;includeRawText=true 保留完整恢复能力。base 的记忆正文及日志本就可能保留问答,本项不扩大为全包脱敏。删除旧任务类型及响应来源,新流程实际使用的任务类型、完成响应和任务列表须同步适配 Memory 与 Backend 两份契约,并通过真实 HTTP 客户端验证。

配套修改必须覆盖:数据库非空约束及升级迁移、“未归属→已归属”的首次绑定、重复提交、来源 hook/扫描、Session 时间线、关闭后的补处理,以及 Memory/Backend 完成接口契约。所有捕获入口和 turnJudge 两种模式都应遵循先保存的规则。回归除模型故障→关闭→重启恢复外,还应验证“L1 已生成但 Episode 匹配失败”时原文和 L1 保留、恢复后只补归属、不重复生成;并覆盖 ABA/ABAB、跨 Session 隔离、无空 Episode、幂等及删除/清空后禁止恢复。

.map((message) => message.content)
.join("\n");
if (system.includes("Judge L1 and User Memory independently")) {
return {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smoke 应显式覆盖 turnJudge 两种模式及异步处理完成状态

当前 head 执行 npm run smoke:memory-layer:test 时,“stores, processes, reads, and recalls a verified turn through the real Memory service” 用例失败:summaryWorker.failed 为 1,预期为 0;指定 base 和上轮 head 的同一文件均为 3 项通过。这是模型替身适配遗漏,不能据此认定真实模型的正常捕获流程失败。

测试直接使用 DEFAULT_MEMMY_CONFIG,默认 turnJudge 改为 true 后会走联合判断;createSmokeSummaryLlm 仍只返回旧版 l1/user,缺少 relation、feedback、reason 和 l1.unverified_answer,新判断校验因此拒绝响应。临时副本补齐字段后通过,只能证明这一格式问题可修,不能替代双模式及新异步流程的验证。

将测试显式参数化为 turnJudge=false 和 true:false 保留旧算法的返回格式与验证,true 提供完整联合判断结果并验证新算法确实被调用。采用“先保存、后台生成 L1 和归属”后,两种模式都分别验证接收完成时原始记录已保存,以及后台完成后的 L1、Episode 关联、索引和召回;不得仍假定完成接口返回时已有 Episode,或用任意固定等待代替处理状态检查。补充可控的匹配模型故障场景,验证 L1 已生成但尚未归属时记录保留,恢复后不重复生成且能完成归属。默认开关另设独立配置测试,避免默认值变化切换唯一 smoke 路径。修正后重跑该 smoke 单测及组合入口;只有旧算法正式移除时,才同步移除 false 路径及配套 smoke。

episodeId: target.id, dedupeKey: `episode_idle_close:${raw.id}`,
payload: { triggerRawTurnId: raw.id, triggerEpisodeId: target.id, triggeredAt: raw.createdAt }, createdAt: at });
}
for (const id of new Set([target.id, base?.id, ...(legacyRoute?.closedEpisodeIds ?? [])])) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

会话关闭后,较早被暂缓的 Episode 可能漏掉反思和评分

问题
同一会话已经保存多轮问答,但后台还没有确定这些原始轮次分别属于哪个任务(Episode)。此时关闭会话,待所有轮次完成归属后,符合条件的已关闭 Episode 都应继续进行反思和评分。
当前实现可能漏掉较早的 Episode。补充测试先保存分别属于三个 Episode 的轮次,再关闭会话并运行后台任务;所有轮次归属后,第一个 Episode 的原文和 L1 记忆都还在,却没有创建反思或评分任务。这里遗漏的是整个 Episode 的后续处理,不是某条 L1 被删除,也不是逐条 L1 评分失败。
该结果通过真实服务、后台任务处理器和 SQLite 复现,模型使用测试替身;分别固定 turnJudge=true、false 均复现,指定 base 和上轮 head 的相同三任务测试均通过。true 模式下不显式指定 Episode、由模型替身判断三个独立任务,也复现了第一个 Episode 未获后续处理。

原因
Episode 关闭时,只要同一个 Session 还有未完成归属的原始轮次,就暂缓该 Episode 的反思和评分。等待的是一轮问答及工具记录的归属;即使剩余轮次最终属于另一个 Episode,也会触发这个等待条件。
等最后一轮完成归属,代码只重新检查当前目标 Episode、上一 Episode 和这次刚关闭的 Episode,没有遍历更早被暂缓的所有 Episode。例如,三个 Episode 的处理先后被暂缓,最后一次检查可能覆盖第三个和第二个,却漏掉第一个。
因此问题不是“只给最后一条 L1 评分”,而是当前自动处理链没有完整补查被暂缓的 Episode;不能依赖后续恰好有其他事件触发它们。

建议做法
在同一 Session 的待归属轮次全部处理完成后,统一检查其中所有已关闭、尚未完成反思或评分的 Episode,按现有条件补建后续任务。复用已有处理状态和去重规则;已经关闭的 Session 保持关闭,不重新接受普通写入。
补充“多个任务的轮次积压→关闭会话→全部完成归属”的回归,并覆盖归属失败后恢复、中途重启和重复恢复。开发测试分别固定 true、false,检查所有符合条件的 Episode 最终完成后续处理、原文和 L1 保留,且没有重复反思或评分。

Revisit every closed Episode in the Session after the last pending turn is assigned, reusing existing finalization and deduplication guards. Cover both turnJudge modes, explicit and model-selected Episodes, quota recovery, interrupted leases, repeated recovery, and open Session finalization.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants