@zooique/memora 5.1.0 → 5.3.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +107 -4
  2. package/README.en.md +6 -2
  3. package/README.md +6 -2
  4. package/dist/agent/agent.d.ts +42 -2
  5. package/dist/agent/agent.d.ts.map +1 -1
  6. package/dist/agent/agent.js +61 -3
  7. package/dist/agent/agent.js.map +1 -1
  8. package/dist/agent/assembler.d.ts.map +1 -1
  9. package/dist/agent/assembler.js +7 -0
  10. package/dist/agent/assembler.js.map +1 -1
  11. package/dist/agent/constants.d.ts +49 -0
  12. package/dist/agent/constants.d.ts.map +1 -1
  13. package/dist/agent/constants.js +49 -0
  14. package/dist/agent/constants.js.map +1 -1
  15. package/dist/agent/contextManager.d.ts +31 -3
  16. package/dist/agent/contextManager.d.ts.map +1 -1
  17. package/dist/agent/contextManager.js +39 -2
  18. package/dist/agent/contextManager.js.map +1 -1
  19. package/dist/agent/loop.d.ts +14 -7
  20. package/dist/agent/loop.d.ts.map +1 -1
  21. package/dist/agent/loop.js +43 -18
  22. package/dist/agent/loop.js.map +1 -1
  23. package/dist/agent/managers/llmCaller.d.ts.map +1 -1
  24. package/dist/agent/managers/llmCaller.js +14 -1
  25. package/dist/agent/managers/llmCaller.js.map +1 -1
  26. package/dist/agent/managers/sessionManager.d.ts.map +1 -1
  27. package/dist/agent/managers/sessionManager.js +1 -0
  28. package/dist/agent/managers/sessionManager.js.map +1 -1
  29. package/dist/agent/managers/toolCallHelpers.d.ts.map +1 -1
  30. package/dist/agent/managers/toolCallHelpers.js +3 -0
  31. package/dist/agent/managers/toolCallHelpers.js.map +1 -1
  32. package/dist/agent/messageHistory.d.ts +86 -16
  33. package/dist/agent/messageHistory.d.ts.map +1 -1
  34. package/dist/agent/messageHistory.js +203 -65
  35. package/dist/agent/messageHistory.js.map +1 -1
  36. package/dist/agent/seed/orchestrator.d.ts +2 -1
  37. package/dist/agent/seed/orchestrator.d.ts.map +1 -1
  38. package/dist/agent/seed/orchestrator.js +48 -17
  39. package/dist/agent/seed/orchestrator.js.map +1 -1
  40. package/dist/agent/seed/prepare.d.ts +3 -1
  41. package/dist/agent/seed/prepare.d.ts.map +1 -1
  42. package/dist/agent/seed/prepare.js +10 -2
  43. package/dist/agent/seed/prepare.js.map +1 -1
  44. package/dist/agent/sessionStateMachine.d.ts +3 -0
  45. package/dist/agent/sessionStateMachine.d.ts.map +1 -1
  46. package/dist/agent/sessionStateMachine.js.map +1 -1
  47. package/dist/agent/textSanitize.d.ts +1 -1
  48. package/dist/agent/textSanitize.js +1 -1
  49. package/dist/agent/types.d.ts +9 -0
  50. package/dist/agent/types.d.ts.map +1 -1
  51. package/dist/index.d.ts +4 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +11 -4
  54. package/dist/index.js.map +1 -1
  55. package/dist/llm/factory.d.ts +21 -0
  56. package/dist/llm/factory.d.ts.map +1 -1
  57. package/dist/llm/factory.js +4 -1
  58. package/dist/llm/factory.js.map +1 -1
  59. package/dist/llm/openaiCompatible.d.ts +8 -1
  60. package/dist/llm/openaiCompatible.d.ts.map +1 -1
  61. package/dist/llm/openaiCompatible.js +26 -0
  62. package/dist/llm/openaiCompatible.js.map +1 -1
  63. package/dist/llm/provider.d.ts +65 -0
  64. package/dist/llm/provider.d.ts.map +1 -1
  65. package/dist/llm/provider.js +10 -0
  66. package/dist/llm/provider.js.map +1 -1
  67. package/dist/memory/gcService.d.ts +8 -4
  68. package/dist/memory/gcService.d.ts.map +1 -1
  69. package/dist/memory/gcService.js.map +1 -1
  70. package/dist/memory/inMemoryRoundStore.d.ts +8 -3
  71. package/dist/memory/inMemoryRoundStore.d.ts.map +1 -1
  72. package/dist/memory/inMemoryRoundStore.js +19 -10
  73. package/dist/memory/inMemoryRoundStore.js.map +1 -1
  74. package/dist/memory/inMemorySessionStore.d.ts.map +1 -1
  75. package/dist/memory/inMemorySessionStore.js +11 -0
  76. package/dist/memory/inMemorySessionStore.js.map +1 -1
  77. package/dist/memory/roundStore.d.ts +84 -11
  78. package/dist/memory/roundStore.d.ts.map +1 -1
  79. package/dist/memory/roundStore.js +18 -0
  80. package/dist/memory/roundStore.js.map +1 -1
  81. package/dist/memory/sessionStore.d.ts +8 -0
  82. package/dist/memory/sessionStore.d.ts.map +1 -1
  83. package/dist/memory/sessionStore.js.map +1 -1
  84. package/dist/utils/time.d.ts +24 -4
  85. package/dist/utils/time.d.ts.map +1 -1
  86. package/dist/utils/time.js +32 -4
  87. package/dist/utils/time.js.map +1 -1
  88. package/docs/memora-api-reference.md +14 -4
  89. package/docs/memora-/346/216/245/345/205/245/346/214/207/345/215/227.md +45 -17
  90. package/package.json +5 -2
@@ -27,8 +27,11 @@ export function parseAskCalls(askCalls) {
27
27
  catch {
28
28
  // 参数非法:降级为空问题(宿主渲染兜底,不阻断)
29
29
  }
30
+ // ADR-043:题身份在此**单点填入**(工具调用 id 即题身份),下游一律透传不自造。
31
+ // 批内第 i 题 ↔ askCalls[i].id,与 AgentLoop.pendingAsk.toolCallIds 同序同源。
30
32
  return {
31
33
  slot: 'ask',
34
+ askCallId: tc.id,
32
35
  question,
33
36
  ...(options && options.length > 0 ? { options } : {}),
34
37
  ...(allowCustom !== undefined ? { allowCustom } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"toolCallHelpers.js","sourceRoot":"","sources":["../../../src/agent/managers/toolCallHelpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"toolCallHelpers.js","sourceRoot":"","sources":["../../../src/agent/managers/toolCallHelpers.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@
6
6
  * 会话仅持有 roundIds,不再另存扁平消息列表。
7
7
  */
8
8
  import type { ISessionStore, SessionMessage } from '../memory/sessionStore.js';
9
+ import type { ImageRef } from '../llm/provider.js';
9
10
  import type { InteractiveInputKind, IRoundStore, RoundEvidenceEvent } from '../memory/roundStore.js';
10
11
  /** 分叉结果(round-based 模式) */
11
12
  export interface ForkResult {
@@ -16,6 +17,19 @@ export interface ForkResult {
16
17
  /** 新会话的 Round ID 列表(已复制的指针) */
17
18
  roundIds: string[];
18
19
  }
20
+ /** 交互输入落档草稿(appendUser opts.group 元素):逐题展开的最小描述,id/timestamp/role 由 buildInteractiveInputs 统一装配 */
21
+ interface InteractiveInputDraft {
22
+ content: string;
23
+ kind: InteractiveInputKind;
24
+ /**
25
+ * 题身份(ADR-043 单源):来自内核 `AskQuestion.askCallId`(发起提问的工具调用 id)。
26
+ * 有则**直接作为落盘 id**(运行时行与盘面行同一身份,去重可靠配对);
27
+ * 无(supplement / 历史数据 / 未带身份的收场转化)则回退轮内序号公式。
28
+ */
29
+ id?: string;
30
+ question?: string;
31
+ options?: string[];
32
+ }
19
33
  /** 消息历史类 */
20
34
  export declare class MessageHistory {
21
35
  /** 会话存储(可选);注入则持久化,否则仅在内存保存(AgentLoop.messages[]) */
@@ -79,24 +93,59 @@ export declare class MessageHistory {
79
93
  /**
80
94
  * 追加 user 消息到当前会话(持久化失败不抛出,不阻塞对话)。
81
95
  *
82
- * Round-based 路径:roundStore 注入且 roundId 存在时,创建 pending Round
83
- * 写入 RoundStore(refCount=0,未完成不登记会话)。**仅 complete 才 appendRoundId**——
84
- * 崩溃残留的 pending 轮是孤儿(refCount=0),可由 GC 清理,不污染会话视图。
96
+ * Round-based 路径:roundStore 注入且 roundId 存在时,创建 pending Round 并**立即**归属当前会话
97
+ * (refCount 1 + appendRoundId,ADR-044「建轮即入会话」)。存在性由**创建**决定、不由**收场**决定——
98
+ * 未收场的轮同样可见、可删、可续跑;崩溃残留的 pending 轮不再是「GC 可清孤儿」,而是用户可见的
99
+ * 未完成轮(GC 只回收「连归属都没写成功」的残留,判据仍是 `refCount === 0`)。
85
100
  *
86
101
  * 交互输入:opts.interactive 为真且 roundId 对应轮已存在(prepare 建 pending 或
87
102
  * 暂停已完成轮)时,不再创建/覆盖新轮——按序追加到该轮 interactiveInputs,保问答闭环不分裂。
88
103
  *
89
104
  * @param content 用户输入内容
90
105
  * @param roundId 闭环节点轮次 ID(可选,交互输入必须携带 = appendUser 的 head roundId)
91
- * @param opts 交互输入选项(interactive=是否交互归属;kind=折叠块类型文案;
92
- * question/options=该回答所对的 ask_user 提问原文与候选选项,随回答一并持久化)
106
+ * @param opts 交互输入选项(interactive=是否交互归属;kind/question/options=单条形态的
107
+ * 折叠块类型与所对 ask_user 提问原文/候选选项;group=整组形态,ASK-MULTI-Q-1:
108
+ * 作答/超时多题批次逐题落档,与崩溃收场转化同宽——提供时按序展开为 N 条,
109
+ * 单条形态等价于 1 元素组,同时提供以 group 为准)
93
110
  */
94
111
  appendUser(content: string, roundId?: string, opts?: {
95
112
  interactive?: boolean;
96
113
  kind?: InteractiveInputKind;
97
114
  question?: string;
98
115
  options?: string[];
116
+ /** 整组交互输入(多题批次逐题落档);元素结构见 InteractiveInputDraft */
117
+ group?: ReadonlyArray<InteractiveInputDraft>;
118
+ /** 图片附件(可选,多模态输入);须显式透传到 `userMessage` 白名单与 SessionMessage,否则重启 / 切会话后图静默消失(方案 §7 P0-12) */
119
+ attachments?: readonly ImageRef[];
99
120
  }): Promise<void>;
121
+ /**
122
+ * 把轮登记进当前会话的 `roundIds`(**ADR-044 归属登记单点**)。
123
+ *
124
+ * 语义:**建轮即登记**,收场不再登记。失败只记日志、不抛出——不阻塞对话。
125
+ *
126
+ * ⚠️ 失败的后果(ADR-044 第四刀写清,勿再按旧口径理解「退回登记前行为」):
127
+ * 轮已按 `refCount: 1` 落盘、但会话 `roundIds` 里没有它 ⇒ **不可见且不会被 GC 回收**
128
+ * (孤儿判据是 `refCount === 0`,它不满足)——即「静默保留、用户看不见也删不掉」。
129
+ * 这是**有意的取舍**:宁可留一个看不见的文件,也不静默删掉用户刚输入的内容;
130
+ * 触发条件 = 会话存储写入抛错(已 warn 日志可查)。
131
+ *
132
+ * 调用面只有三处,判据各不相同(勿扩张):
133
+ * - `appendUser` 建轮后(新轮必登记);
134
+ * - `appendAssistant` 兜底分支(轮原本不存在,新建即归属);
135
+ * - `attachUnownedRound`(存量 `refCount 0` 轮补登记,见其注释)。
136
+ */
137
+ private registerRoundInSession;
138
+ /**
139
+ * 归属兜底(ADR-044 兼容面):`refCount === 0` 的轮 = **ADR-044 之前**落盘的存量
140
+ * (当时建轮写 0、收场才 +1),收场时补到 1 并登记进会话。
141
+ *
142
+ * 为什么不能直接删掉收场侧登记:存量半截轮(pending + refCount 0)在升级后仍必须可见,
143
+ * 删掉登记会让它们**永久不可见**——比「被 GC 回收」更糟(用户连删都删不到)。
144
+ * 新数据建轮即 refCount 1,本方法是 no-op。
145
+ *
146
+ * @returns 需要落盘的 Round(refCount 可能被补到 1);调用方负责 save。
147
+ */
148
+ private attachUnownedRound;
100
149
  /**
101
150
  * 追加裁决证据到在途 Round(悬案取证轨;持久化失败不抛出,不阻塞对话)。
102
151
  *
@@ -109,13 +158,32 @@ export declare class MessageHistory {
109
158
  * @param ev 裁决证据事件(空响应 / 台账替身回显个案证据)
110
159
  */
111
160
  appendEvidence(roundId: string, ev: RoundEvidenceEvent): Promise<void>;
161
+ /**
162
+ * 登记在途提问到当前轮(E2E-ASK-LOSS-1 写点;持久化失败不抛出)。
163
+ *
164
+ * 调用时机 = 提问挂起(yield paused)**之前**(装配层经 loop.onPendingQuestion 接线),
165
+ * 保证「AI 已提问」这一事实先于挂起落盘——挂起后崩溃,问题文本仍可从盘上恢复,
166
+ * 不再只活在进程内存(loop.pendingAsk)。
167
+ *
168
+ * 形状收窄:入参直取内核 `AskQuestion`,落盘只留 `question/options` 两个重放必需面
169
+ * (见 `RoundPendingAsk`——slot/allowCustom 是交互期参数,历史重放不消费)。
170
+ * 轮缺失(异常兜底):warn 后丢弃——挂起轮由 appendUser 建出,缺失即更上游已异常,
171
+ * 此处不伪造第二轨道补写(与 appendEvidence 同款降级语义)。
172
+ *
173
+ * @param roundId 闭环节点轮次 ID(loop.getCurrentRoundId(),与 appendUser 同源同值)
174
+ * @param questions 本批挂起提问(ask_user 解析产物,可多题)
175
+ */
176
+ markPendingAsk(roundId: string, questions: readonly {
177
+ question: string;
178
+ options?: string[];
179
+ }[]): Promise<void>;
112
180
  /**
113
181
  * 追加 assistant 消息到当前会话(持久化失败不抛出)。
114
182
  *
115
- * Round-based 路径:完成 pending Round 时**才登记会话引用**——
116
- * incrementRef(refCount 0→1)+ appendRoundId。pending 缓存丢失(跨重启)时从
117
- * RoundStore 取回补全;Round 不存在(异常兜底)时创建独立 complete Round 并登记,
118
- * 保证用户消息不丢失。
183
+ * Round-based 路径:完成 pending Round 时**只改状态**——归属已在建轮时登记(ADR-044),
184
+ * 此处不再 incrementRef / appendRoundId;`refCount === 0` 的存量轮由 attachUnownedRound 补登记。
185
+ * pending 缓存丢失(跨重启)时从 RoundStore 取回补全;Round 不存在(异常兜底)时创建独立
186
+ * complete Round 并登记,保证用户消息不丢失。
119
187
  *
120
188
  * 闭环节点续写:同一 roundId 已存在 assistantMessage(跨暂停-续跑:
121
189
  * 暂停轮已把 ask_user 提问/中断半截落为 assistantMessage)时,旧段入 assistantLog,
@@ -126,10 +194,12 @@ export declare class MessageHistory {
126
194
  /**
127
195
  * 中断轮收场:把「未正常完成」的轮升级为正常 stop turn 并入会话。
128
196
  *
129
- * 语义定案(step-atomic-persistence.md §一·五):崩溃残留轮(pending/error + refCount=0 + 有已
130
- * 落盘 processEvents)= 等同于用户点「停止」的正常 turn——可删、入会话 roundIds、作后续上下文,
131
- * **不是**半成品草稿/孤儿。宿主经 `IRoundStore.listInterruptedRecent` 打捞后再调本方法完成
132
- * 「升级登记」,恢复为普通 turn 渲染无需特殊草稿卡。
197
+ * 语义定案(step-atomic-persistence.md §一·五):崩溃残留轮 = 等同于用户点「停止」的正常 turn
198
+ * ——可删、入会话 roundIds、作后续上下文,**不是**半成品草稿/孤儿。
199
+ * ⚠️ ADR-044 起:新建轮**建轮即入会话**,故本方法的「入会话」动作只对**存量无归属轮**
200
+ * (`refCount === 0`,ADR-044 之前落盘或建轮时登记失败)生效,见 attachUnownedRound。
201
+ * 宿主经 `IRoundStore.listInterruptedRecent` 打捞后再调本方法完成收场,恢复为普通 turn
202
+ * 渲染无需特殊草稿卡。
133
203
  *
134
204
  * **两个调用时机**:
135
205
  * 1. 崩溃/断电后重启——宿主 `chatPanel.upgradeInterruptedRounds` 打捞后调用;
@@ -138,9 +208,8 @@ export declare class MessageHistory {
138
208
  * 留下 pending 孤儿轮、须等下次重启才打捞(LLM 4xx 中断的长任务轮即此类)。
139
209
  * 依据 = `memory/roundStore.ts` RoundStatus 文档「运行时失败不翻状态机,一律按中断处理」。
140
210
  *
141
- * 收场约定与 appendAssistant 同一真理源(refCount 0→1 + appendRoundId + status interrupted +
142
- * completedAt + isReappend 防重复登记,两处终态判据同为 `isRoundSettled`),Round schema /
143
- * 存储格式不变;
211
+ * 收场约定与 appendAssistant 同一真理源(status interrupted + completedAt + 存量无归属轮补登记
212
+ * 同走 attachUnownedRound;「已收场即跳过」仍用 `isRoundSettled`),Round schema / 存储格式不变;
144
213
  * **有意不直接复用 appendAssistant**,两处差异:
145
214
  * - appendAssistant 顶层跳过空内容 → 无文本中断轮无法收场;本方法允许多段「即使无
146
215
  * assistantMessage 总结也按 stop 语义收场」(§一·五验收口径)。
@@ -183,4 +252,5 @@ export declare class MessageHistory {
183
252
  */
184
253
  awaitPendingArchives(timeoutMs?: number): Promise<boolean>;
185
254
  }
255
+ export {};
186
256
  //# sourceMappingURL=messageHistory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"messageHistory.d.ts","sourceRoot":"","sources":["../../src/agent/messageHistory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAe,MAAM,0BAA0B,CAAC;AAE3F,OAAO,KAAK,EACV,oBAAoB,EACpB,WAAW,EAEX,kBAAkB,EAGnB,MAAM,wBAAwB,CAAC;AAOhC,2BAA2B;AAC3B,MAAM,WAAW,UAAU;IACzB,2BAA2B;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW;IACX,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,YAAY;AACZ,qBAAa,cAAc;IAWvB,qDAAqD;IACrD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAG9B,iDAAiD;IACjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAf9B,sBAAsB;IACtB,OAAO,CAAC,WAAW,CAAS;IAC5B,qBAAqB;IACrB,OAAO,CAAC,cAAc,CAAS;IAC/B,mDAAmD;IACnD,OAAO,CAAC,eAAe,CAAoC;IAC3D,6DAA6D;IAC7D,OAAO,CAAC,aAAa,CAAiC;;IAGpD,qDAAqD;IACpC,YAAY,CAAC,EAAE,aAAa,YAAA,EAC7C,WAAW,CAAC,EAAE,MAAM,EACpB,cAAc,SAAS;IACvB,iDAAiD;IAChC,UAAU,CAAC,EAAE,WAAW,YAAA;IAM3C,8BAA8B;IAC9B,IAAI,kBAAkB,IAAI,MAAM,CAE/B;IAED,sBAAsB;IACtB,IAAI,gBAAgB,IAAI,MAAM,CAE7B;IAED,aAAa;IACb,IAAI,mBAAmB,IAAI,MAAM,CAEhC;IAED;;;;OAIG;IACH,eAAe,IAAI,MAAM,GAAG,IAAI;IAchC,aAAa;IACb,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,mBAAmB;IACnB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAOzC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAO7B;;;;;;;;;;OAUG;IACH,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,UAAU;IA6EjE;;;;;;;;;;;;;;OAcG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;QACL,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,IAAI,CAAC,EAAE,oBAAoB,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,GACA,OAAO,CAAC,IAAI,CAAC;IA6DhB;;;;;;;;;;OAUG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB5E;;;;;;;;;;;;OAYG;IACG,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkGvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,iBAAiB,CACrB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,GACpD,OAAO,CAAC,IAAI,CAAC;IA0DhB,yDAAyD;IACnD,eAAe,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQ1C,kEAAkE;IAC5D,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAYnF;;;;;;;;OAQG;IACH,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE;IA8B5D,8EAA8E;IAC9E,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI;IAOjD;;;;;OAKG;IACG,oBAAoB,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,OAAO,CAAC;CAiB/D"}
1
+ {"version":3,"file":"messageHistory.d.ts","sourceRoot":"","sources":["../../src/agent/messageHistory.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,25 @@ import { logger } from '../logging/logger.js';
4
4
  import { configError } from '../utils/errors.js';
5
5
  import { safeSetTimeout } from '../utils/safeTimer.js';
6
6
  import { nowIso, todayDate } from '../utils/time.js';
7
+ /**
8
+ * 整组交互输入 → 逐题 RoundInteractiveInput(ASK-MULTI-Q-1 单点装配):
9
+ * id 公式 / role / timestamp / question-options 摊开条件**只在此一处**。
10
+ * appendUser 交互分支(作答/超时整组)与 appendInterrupted 收场转化(unanswered 整组)
11
+ * 共用同一装配,两条消费路径从源头同宽。
12
+ */
13
+ function buildInteractiveInputs(roundId, existingCount, drafts, timestamp) {
14
+ return drafts.map((d, i) => ({
15
+ // ADR-043:题身份优先(内核 toolCallId,跨重启恒定 ⇒ 运行时与重放可配对);
16
+ // 缺省回退轮内序号(supplement 等无题身份来源的路径,行为与历史一致)。
17
+ id: d.id ?? `msg-${roundId}-input-${existingCount + i + 1}`,
18
+ role: 'user',
19
+ content: d.content,
20
+ timestamp,
21
+ kind: d.kind,
22
+ ...(d.question ? { question: d.question } : {}),
23
+ ...(d.options && d.options.length > 0 ? { options: d.options } : {}),
24
+ }));
25
+ }
7
26
  /** 消息历史类 */
8
27
  export class MessageHistory {
9
28
  sessionStore;
@@ -173,17 +192,20 @@ export class MessageHistory {
173
192
  /**
174
193
  * 追加 user 消息到当前会话(持久化失败不抛出,不阻塞对话)。
175
194
  *
176
- * Round-based 路径:roundStore 注入且 roundId 存在时,创建 pending Round
177
- * 写入 RoundStore(refCount=0,未完成不登记会话)。**仅 complete 才 appendRoundId**——
178
- * 崩溃残留的 pending 轮是孤儿(refCount=0),可由 GC 清理,不污染会话视图。
195
+ * Round-based 路径:roundStore 注入且 roundId 存在时,创建 pending Round 并**立即**归属当前会话
196
+ * (refCount 1 + appendRoundId,ADR-044「建轮即入会话」)。存在性由**创建**决定、不由**收场**决定——
197
+ * 未收场的轮同样可见、可删、可续跑;崩溃残留的 pending 轮不再是「GC 可清孤儿」,而是用户可见的
198
+ * 未完成轮(GC 只回收「连归属都没写成功」的残留,判据仍是 `refCount === 0`)。
179
199
  *
180
200
  * 交互输入:opts.interactive 为真且 roundId 对应轮已存在(prepare 建 pending 或
181
201
  * 暂停已完成轮)时,不再创建/覆盖新轮——按序追加到该轮 interactiveInputs,保问答闭环不分裂。
182
202
  *
183
203
  * @param content 用户输入内容
184
204
  * @param roundId 闭环节点轮次 ID(可选,交互输入必须携带 = appendUser 的 head roundId)
185
- * @param opts 交互输入选项(interactive=是否交互归属;kind=折叠块类型文案;
186
- * question/options=该回答所对的 ask_user 提问原文与候选选项,随回答一并持久化)
205
+ * @param opts 交互输入选项(interactive=是否交互归属;kind/question/options=单条形态的
206
+ * 折叠块类型与所对 ask_user 提问原文/候选选项;group=整组形态,ASK-MULTI-Q-1:
207
+ * 作答/超时多题批次逐题落档,与崩溃收场转化同宽——提供时按序展开为 N 条,
208
+ * 单条形态等价于 1 元素组,同时提供以 group 为准)
187
209
  */
188
210
  async appendUser(content, roundId, opts) {
189
211
  const message = {
@@ -191,6 +213,7 @@ export class MessageHistory {
191
213
  content,
192
214
  timestamp: nowIso(),
193
215
  ...(roundId ? { roundId } : {}),
216
+ ...(opts?.attachments ? { attachments: opts.attachments } : {}),
194
217
  };
195
218
  // 会话日期锚点不随输入漂移:currentDate 由显式加载/新建决定,跨天续聊沿用原会话日期
196
219
  // Round-based 路径:roundStore 注入且有 roundId 时写入
@@ -200,28 +223,42 @@ export class MessageHistory {
200
223
  if (opts?.interactive) {
201
224
  const existing = this.roundStore.getById(roundId) ?? this.pendingRounds.get(roundId);
202
225
  if (existing) {
203
- const input = {
204
- id: `msg-${roundId}-input-${(existing.interactiveInputs?.length ?? 0) + 1}`,
205
- role: 'user',
206
- content,
207
- timestamp: message.timestamp,
208
- kind: opts.kind ?? 'supplement',
209
- // 提问原文/选项随回答落盘(question-answer 携带;supplement/缺省不落,旧数据向后兼容)
210
- ...(opts.question ? { question: opts.question } : {}),
211
- ...(opts.options && opts.options.length > 0 ? { options: opts.options } : {}),
212
- };
226
+ // 单条形态 = 退化 1 元素组:一条装配路径承载两种入参(无分叉实现)
227
+ const drafts = opts.group ?? [
228
+ {
229
+ content,
230
+ kind: opts.kind ?? 'supplement',
231
+ ...(opts.question ? { question: opts.question } : {}),
232
+ ...(opts.options && opts.options.length > 0 ? { options: opts.options } : {}),
233
+ },
234
+ ];
235
+ const inputs = buildInteractiveInputs(roundId, existing.interactiveInputs?.length ?? 0, drafts, message.timestamp);
236
+ // 作答/超时记录落盘的**同一次 save 内**原子清 pendingAsk(生命周期不变量的清除点,
237
+ // 见 RoundPendingAsk 注释):提问已被消费(回答回填或超时收场),残留即脏数据——
238
+ // 若留到收场才清,「已答 + 未答」双记录窗口会打开(appendInterrupted 会把残留
239
+ // pendingAsk 再转化成重复的 unanswered 条目)。supplement 与提问无关,不动该字段。
240
+ // 判据含 unanswered(ASK-MULTI-Q-1):批次内全跳过(逐题均 unanswered)同样代表
241
+ // 快照已被 takeAnsweredAsk 消费——不清则收场转化二次记账。
242
+ // ⚠️ 勿在此点之前另加「提前清盘」层:答案此刻**尚未落盘**(`AgentLoop.appendToolMessage`
243
+ // 只 push 进程内 `loop.messages`,`Round` 结构不承载 tool 消息),二者之间不存在
244
+ // 「答案已持久而 pendingAsk 未清」的中间态 ⇒ 提前清的唯一净效应是把崩溃后一条
245
+ // 诚实的 unanswered 记录换成「该题在盘面彻底消失」(2026-10-31 实测,据此回滚)。
246
+ const askConsumed = drafts.some((d) => d.kind === 'question-answer' || d.kind === 'timeout' || d.kind === 'unanswered');
213
247
  const updated = {
214
248
  ...existing,
215
- interactiveInputs: [...(existing.interactiveInputs ?? []), input],
249
+ interactiveInputs: [...(existing.interactiveInputs ?? []), ...inputs],
250
+ ...(askConsumed ? { pendingAsk: undefined } : {}),
216
251
  };
217
252
  this.roundStore.save(updated);
218
253
  this.pendingRounds.set(roundId, updated);
219
- logger.debug({ roundId, kind: input.kind }, 'appendUser: 交互输入归属问答闭环');
254
+ logger.debug({ roundId, count: inputs.length, firstKind: inputs[0]?.kind }, 'appendUser: 交互输入归属问答闭环');
220
255
  return;
221
256
  }
222
257
  // 轮不存在(异常兜底):降级为普通 pending Round(内容不丢失,GC 兜底)
223
258
  logger.warn({ roundId }, 'appendUser: 交互输入对应轮缺失,降级为新建 pending Round');
224
259
  }
260
+ // 重复 appendUser 到同一 roundId(异常路径)不重复登记,防 roundIds 堆叠
261
+ const preexisting = this.roundStore.getById(roundId) ?? this.pendingRounds.get(roundId);
225
262
  const pendingRound = {
226
263
  id: roundId,
227
264
  userMessage: {
@@ -229,15 +266,21 @@ export class MessageHistory {
229
266
  role: 'user',
230
267
  content,
231
268
  timestamp: message.timestamp,
269
+ ...(opts?.attachments ? { attachments: opts.attachments } : {}),
232
270
  },
233
271
  status: 'pending',
234
272
  createdAt: message.timestamp,
235
- // 未完成轮不被任何会话引用(complete 时 incrementRef + appendRoundId)
236
- refCount: 0,
273
+ // 🔴 ADR-044 建轮即入会话:存在性由「创建」决定,不由「收场」决定。
274
+ // 创建它的会话立即持有引用(refCount 1)⇒ 会话视图立即可见、可删、可续跑。
275
+ // `save()` 会用 round.refCount 重建索引条目(workspaceRoundStore.save),故此处**不再**调
276
+ // incrementRef——那既二次落盘,又让「建轮」与「收场」两处并改写同一个计数(两个真相源)。
277
+ refCount: 1,
237
278
  };
238
279
  this.roundStore.save(pendingRound);
239
280
  this.pendingRounds.set(roundId, pendingRound);
240
- logger.debug({ roundId }, 'appendUser: Round-based pending Round created');
281
+ if (!preexisting)
282
+ this.registerRoundInSession(roundId);
283
+ logger.debug({ roundId }, 'appendUser: pending Round created(已归属当前会话)');
241
284
  }
242
285
  catch (err) {
243
286
  logger.warn({ err, roundId }, 'appendUser: Round-based 写入失败');
@@ -245,6 +288,48 @@ export class MessageHistory {
245
288
  }
246
289
  logger.debug({ role: message.role, session: this.currentSessionName }, 'appendUser');
247
290
  }
291
+ /**
292
+ * 把轮登记进当前会话的 `roundIds`(**ADR-044 归属登记单点**)。
293
+ *
294
+ * 语义:**建轮即登记**,收场不再登记。失败只记日志、不抛出——不阻塞对话。
295
+ *
296
+ * ⚠️ 失败的后果(ADR-044 第四刀写清,勿再按旧口径理解「退回登记前行为」):
297
+ * 轮已按 `refCount: 1` 落盘、但会话 `roundIds` 里没有它 ⇒ **不可见且不会被 GC 回收**
298
+ * (孤儿判据是 `refCount === 0`,它不满足)——即「静默保留、用户看不见也删不掉」。
299
+ * 这是**有意的取舍**:宁可留一个看不见的文件,也不静默删掉用户刚输入的内容;
300
+ * 触发条件 = 会话存储写入抛错(已 warn 日志可查)。
301
+ *
302
+ * 调用面只有三处,判据各不相同(勿扩张):
303
+ * - `appendUser` 建轮后(新轮必登记);
304
+ * - `appendAssistant` 兜底分支(轮原本不存在,新建即归属);
305
+ * - `attachUnownedRound`(存量 `refCount 0` 轮补登记,见其注释)。
306
+ */
307
+ registerRoundInSession(roundId) {
308
+ const sessionId = this.currentSessionName;
309
+ try {
310
+ this.sessionStore?.appendRoundId(sessionId, roundId);
311
+ logger.debug({ roundId, sessionId }, 'appendRoundId: 轮已归属当前会话');
312
+ }
313
+ catch (err) {
314
+ logger.warn({ err, roundId, sessionId }, 'appendRoundId: 归属登记失败(轮不可见,不阻断)');
315
+ }
316
+ }
317
+ /**
318
+ * 归属兜底(ADR-044 兼容面):`refCount === 0` 的轮 = **ADR-044 之前**落盘的存量
319
+ * (当时建轮写 0、收场才 +1),收场时补到 1 并登记进会话。
320
+ *
321
+ * 为什么不能直接删掉收场侧登记:存量半截轮(pending + refCount 0)在升级后仍必须可见,
322
+ * 删掉登记会让它们**永久不可见**——比「被 GC 回收」更糟(用户连删都删不到)。
323
+ * 新数据建轮即 refCount 1,本方法是 no-op。
324
+ *
325
+ * @returns 需要落盘的 Round(refCount 可能被补到 1);调用方负责 save。
326
+ */
327
+ attachUnownedRound(round) {
328
+ if (round.refCount > 0)
329
+ return round;
330
+ this.registerRoundInSession(round.id);
331
+ return { ...round, refCount: 1 };
332
+ }
248
333
  /**
249
334
  * 追加裁决证据到在途 Round(悬案取证轨;持久化失败不抛出,不阻塞对话)。
250
335
  *
@@ -274,13 +359,50 @@ export class MessageHistory {
274
359
  logger.warn({ err, roundId }, 'appendEvidence: 写入失败');
275
360
  }
276
361
  }
362
+ /**
363
+ * 登记在途提问到当前轮(E2E-ASK-LOSS-1 写点;持久化失败不抛出)。
364
+ *
365
+ * 调用时机 = 提问挂起(yield paused)**之前**(装配层经 loop.onPendingQuestion 接线),
366
+ * 保证「AI 已提问」这一事实先于挂起落盘——挂起后崩溃,问题文本仍可从盘上恢复,
367
+ * 不再只活在进程内存(loop.pendingAsk)。
368
+ *
369
+ * 形状收窄:入参直取内核 `AskQuestion`,落盘只留 `question/options` 两个重放必需面
370
+ * (见 `RoundPendingAsk`——slot/allowCustom 是交互期参数,历史重放不消费)。
371
+ * 轮缺失(异常兜底):warn 后丢弃——挂起轮由 appendUser 建出,缺失即更上游已异常,
372
+ * 此处不伪造第二轨道补写(与 appendEvidence 同款降级语义)。
373
+ *
374
+ * @param roundId 闭环节点轮次 ID(loop.getCurrentRoundId(),与 appendUser 同源同值)
375
+ * @param questions 本批挂起提问(ask_user 解析产物,可多题)
376
+ */
377
+ async markPendingAsk(roundId, questions) {
378
+ if (!this.roundStore || !roundId || questions.length === 0)
379
+ return;
380
+ try {
381
+ const existing = this.roundStore.getById(roundId) ?? this.pendingRounds.get(roundId);
382
+ if (!existing) {
383
+ logger.warn({ roundId }, 'markPendingAsk: 对应轮缺失,在途提问不落盘');
384
+ return;
385
+ }
386
+ const snapshot = questions.map((q) => ({
387
+ question: q.question,
388
+ ...(q.options && q.options.length > 0 ? { options: [...q.options] } : {}),
389
+ }));
390
+ const updated = { ...existing, pendingAsk: snapshot };
391
+ this.roundStore.save(updated);
392
+ this.pendingRounds.set(roundId, updated);
393
+ logger.debug({ roundId, count: snapshot.length }, 'markPendingAsk: 在途提问已落盘');
394
+ }
395
+ catch (err) {
396
+ logger.warn({ err, roundId }, 'markPendingAsk: 写入失败');
397
+ }
398
+ }
277
399
  /**
278
400
  * 追加 assistant 消息到当前会话(持久化失败不抛出)。
279
401
  *
280
- * Round-based 路径:完成 pending Round 时**才登记会话引用**——
281
- * incrementRef(refCount 0→1)+ appendRoundId。pending 缓存丢失(跨重启)时从
282
- * RoundStore 取回补全;Round 不存在(异常兜底)时创建独立 complete Round 并登记,
283
- * 保证用户消息不丢失。
402
+ * Round-based 路径:完成 pending Round 时**只改状态**——归属已在建轮时登记(ADR-044),
403
+ * 此处不再 incrementRef / appendRoundId;`refCount === 0` 的存量轮由 attachUnownedRound 补登记。
404
+ * pending 缓存丢失(跨重启)时从 RoundStore 取回补全;Round 不存在(异常兜底)时创建独立
405
+ * complete Round 并登记,保证用户消息不丢失。
284
406
  *
285
407
  * 闭环节点续写:同一 roundId 已存在 assistantMessage(跨暂停-续跑:
286
408
  * 暂停轮已把 ask_user 提问/中断半截落为 assistantMessage)时,旧段入 assistantLog,
@@ -324,6 +446,9 @@ export class MessageHistory {
324
446
  assistantLog: assistantLog && assistantLog.length > 0 ? assistantLog : undefined,
325
447
  status: 'complete',
326
448
  completedAt: message.timestamp,
449
+ // 防御清残留(正常完成轮不该有在途提问):作答记录落盘时已原子清(appendUser
450
+ // 交互分支),此处清异常窗口残留;终态轮不携带 pendingAsk(RoundPendingAsk 生命周期不变量)
451
+ pendingAsk: undefined,
327
452
  };
328
453
  this.pendingRounds.delete(roundId);
329
454
  }
@@ -331,25 +456,12 @@ export class MessageHistory {
331
456
  logger.warn({ roundId }, 'appendAssistant: Round not found in RoundStore');
332
457
  }
333
458
  if (completed) {
334
- this.roundStore.save(completed);
335
- // 首次收场才登记会话引用(refCount 0→1 + 列表登记),崩溃残留 pending 保持孤儿可由 GC 清理。
336
- // 续写同一闭环节点(assistantLog:跨暂停-续跑多次 appendAssistant 到同一 roundId)不重复登记——
337
- // 否则 roundIds 同 id 重复堆叠、refCount 虚增,会话视图出现「一个问答闭环多次登记」
338
- // 判据 = isRoundSettled(非自写 'complete'):已是终态(含 interrupted)就不得再登记一次
339
- const isReappend = existing !== null && existing !== undefined && isRoundSettled(existing.status);
340
- if (!isReappend) {
341
- this.roundStore.incrementRef(roundId);
342
- const sessionId = this.currentSessionName;
343
- try {
344
- this.sessionStore?.appendRoundId(sessionId, roundId);
345
- }
346
- catch (err) {
347
- logger.warn({ err, sessionId, roundId }, 'appendAssistant: appendRoundId 失败');
348
- }
349
- }
350
- else {
351
- logger.debug({ roundId, status: existing?.status }, 'appendAssistant: 闭环节点续写段但不重复登记会话引用');
352
- }
459
+ // 🔴 ADR-044:归属已在**建轮**时登记(appendUser),收场只改状态——不再登记、不再加计数。
460
+ // 仅当轮是「存量无归属」(refCount 0,ADR-044 之前落盘)时由 attachUnownedRound 补登记;
461
+ // 直接删掉收场侧登记会让存量半截轮永久不可见,比「不可回收」更糟。
462
+ // 由此 `isRoundSettled` 不再承担「防重复登记」职责(收场已不登记),但保留给
463
+ // appendInterrupted 的「已收场即跳过」幂等判据——两处语义不同,勿合并。
464
+ this.roundStore.save(this.attachUnownedRound(completed));
353
465
  logger.debug({ roundId }, 'appendAssistant: Round-based Round completed');
354
466
  }
355
467
  else {
@@ -374,12 +486,8 @@ export class MessageHistory {
374
486
  refCount: 1,
375
487
  };
376
488
  this.roundStore.save(fallback);
377
- try {
378
- this.sessionStore.appendRoundId(this.currentSessionName, roundId);
379
- }
380
- catch (err) {
381
- logger.warn({ err, roundId }, 'appendAssistant: 兜底 appendRoundId 失败');
382
- }
489
+ // 兜底轮是「新建即归属」:走 ADR-044 单点(不再手写 try/catch 与 sessionStore 断言)。
490
+ this.registerRoundInSession(roundId);
383
491
  }
384
492
  }
385
493
  logger.debug({ role: message.role, session: this.currentSessionName }, 'appendAssistant');
@@ -387,10 +495,12 @@ export class MessageHistory {
387
495
  /**
388
496
  * 中断轮收场:把「未正常完成」的轮升级为正常 stop turn 并入会话。
389
497
  *
390
- * 语义定案(step-atomic-persistence.md §一·五):崩溃残留轮(pending/error + refCount=0 + 有已
391
- * 落盘 processEvents)= 等同于用户点「停止」的正常 turn——可删、入会话 roundIds、作后续上下文,
392
- * **不是**半成品草稿/孤儿。宿主经 `IRoundStore.listInterruptedRecent` 打捞后再调本方法完成
393
- * 「升级登记」,恢复为普通 turn 渲染无需特殊草稿卡。
498
+ * 语义定案(step-atomic-persistence.md §一·五):崩溃残留轮 = 等同于用户点「停止」的正常 turn
499
+ * ——可删、入会话 roundIds、作后续上下文,**不是**半成品草稿/孤儿。
500
+ * ⚠️ ADR-044 起:新建轮**建轮即入会话**,故本方法的「入会话」动作只对**存量无归属轮**
501
+ * (`refCount === 0`,ADR-044 之前落盘或建轮时登记失败)生效,见 attachUnownedRound。
502
+ * 宿主经 `IRoundStore.listInterruptedRecent` 打捞后再调本方法完成收场,恢复为普通 turn
503
+ * 渲染无需特殊草稿卡。
394
504
  *
395
505
  * **两个调用时机**:
396
506
  * 1. 崩溃/断电后重启——宿主 `chatPanel.upgradeInterruptedRounds` 打捞后调用;
@@ -399,9 +509,8 @@ export class MessageHistory {
399
509
  * 留下 pending 孤儿轮、须等下次重启才打捞(LLM 4xx 中断的长任务轮即此类)。
400
510
  * 依据 = `memory/roundStore.ts` RoundStatus 文档「运行时失败不翻状态机,一律按中断处理」。
401
511
  *
402
- * 收场约定与 appendAssistant 同一真理源(refCount 0→1 + appendRoundId + status interrupted +
403
- * completedAt + isReappend 防重复登记,两处终态判据同为 `isRoundSettled`),Round schema /
404
- * 存储格式不变;
512
+ * 收场约定与 appendAssistant 同一真理源(status interrupted + completedAt + 存量无归属轮补登记
513
+ * 同走 attachUnownedRound;「已收场即跳过」仍用 `isRoundSettled`),Round schema / 存储格式不变;
405
514
  * **有意不直接复用 appendAssistant**,两处差异:
406
515
  * - appendAssistant 顶层跳过空内容 → 无文本中断轮无法收场;本方法允许多段「即使无
407
516
  * assistantMessage 总结也按 stop 语义收场」(§一·五验收口径)。
@@ -435,6 +544,22 @@ export class MessageHistory {
435
544
  }
436
545
  // 有恢复文本才写 assistantMessage(§一·五:无 assistantMessage 总结也按 stop 语义收场)
437
546
  const content = opts?.content?.trim() ?? '';
547
+ // 在途提问收场转化(E2E-ASK-LOSS-1):轮上残留 pendingAsk = 「问了但未答且作答记录
548
+ // 未落盘」的窗口仍开着——收场即窗口关闭,转化成 kind:'unanswered' 交互输入并清字段,
549
+ // 问题文本与候选选项从「进程内存」归位到「盘上重放面」(重放渲染复用 timeout 同型「未回答」行)。
550
+ // 消费点统一在本方法(打捞收场 / 运行期失败收场共用),宿主打捞链零改动。
551
+ const pendingAsk = existing.pendingAsk;
552
+ // 多题整组转化(每题一条 unanswered):pendingAsk 是整组快照,逐题保留 question/options——
553
+ // 崩溃轮的问题文本只在收场这一刻归档进 interactiveInputs,取首问即丢 Q2..N。
554
+ // 装配走 buildInteractiveInputs 单点(与 appendUser 作答/超时整组同宽,ASK-MULTI-Q-1)。
555
+ const unansweredInputs = pendingAsk?.length
556
+ ? buildInteractiveInputs(roundId, existing.interactiveInputs?.length ?? 0, pendingAsk.map((q) => ({
557
+ content: LOOP_CONSTANTS.ASK_UNANSWERED_NOTICE,
558
+ kind: 'unanswered',
559
+ question: q.question,
560
+ options: q.options,
561
+ })), nowIso())
562
+ : [];
438
563
  const completed = {
439
564
  // 展开保留 userMessage / interactiveInputs / processEvents 等原字段
440
565
  //(existing 取自 RoundStore 时含宿主检查点已落盘的 processEvents——store-first 优先保证)
@@ -449,20 +574,21 @@ export class MessageHistory {
449
574
  },
450
575
  }
451
576
  : {}),
577
+ ...(unansweredInputs.length
578
+ ? {
579
+ interactiveInputs: [...(existing.interactiveInputs ?? []), ...unansweredInputs],
580
+ }
581
+ : {}),
452
582
  status: 'interrupted',
453
583
  completedAt: nowIso(),
584
+ // 生命周期不变量收口:转化后清字段(终态轮不残留 pendingAsk)
585
+ pendingAsk: undefined,
454
586
  };
455
587
  this.pendingRounds.delete(roundId);
456
- this.roundStore.save(completed);
457
- // 收场登记:与 appendAssistant 同一约定——refCount 0→1 + 会话 roundIds 追加
458
- this.roundStore.incrementRef(roundId);
588
+ // 🔴 ADR-044:归属已在建轮时登记,收场只改状态。**但存量无归属轮(refCount 0)必须补登记**——
589
+ // 跨重启打捞的残留轮正是这一类,省掉它会让打捞出来的轮在会话视图里看不见。
590
+ this.roundStore.save(this.attachUnownedRound(completed));
459
591
  const sessionId = this.currentSessionName;
460
- try {
461
- this.sessionStore?.appendRoundId(sessionId, roundId);
462
- }
463
- catch (err) {
464
- logger.warn({ err, sessionId, roundId }, 'appendInterrupted: appendRoundId 失败');
465
- }
466
592
  logger.info({ roundId, sessionId, hasAssistant: content.length > 0 }, 'appendInterrupted: 崩溃残留轮升级为正常 stop turn');
467
593
  }
468
594
  catch (err) {
@@ -502,18 +628,30 @@ export class MessageHistory {
502
628
  if (!this.roundStore || !roundIds.length)
503
629
  return [];
504
630
  const messages = [];
631
+ const currentIdx = roundIds.length - 1;
505
632
  for (const roundId of roundIds) {
506
633
  const round = this.roundStore.getById(roundId);
507
634
  if (!round) {
508
635
  logger.warn({ roundId }, 'loadRoundBasedMessages: Round 不存在,跳过');
509
636
  continue;
510
637
  }
638
+ // 历史图片窗口衰减(方案 §5.12 / D22 / D23):只有最近 RETAIN_IMAGE_ROUNDS 轮内的
639
+ // attachments 带上模型上下文;更早轮次不带(content 不变,仍是那句占位)。
640
+ // 窗口 = `当前索引 − 消息索引`,纯计算、零状态。
641
+ // 注意该常量是**四个消费点同值**(见 LOOP_CONSTANTS.RETAIN_IMAGE_ROUNDS 的注释):
642
+ // 此处窗口衰减 + 两个 `loadMessages` 实现的同判据衰减 + 宿主会话加载期预读
643
+ // (`hosts/memora-vscode/.../imageCache.ts` 的 `preloadSession`),
644
+ // 拆分条件的完整论证见该常量注释。
645
+ const idx = roundIds.indexOf(roundId);
646
+ const withImages = currentIdx - idx < LOOP_CONSTANTS.RETAIN_IMAGE_ROUNDS;
647
+ const userAttachments = withImages ? round.userMessage.attachments : undefined;
511
648
  // User message
512
649
  messages.push({
513
650
  role: round.userMessage.role,
514
651
  content: round.userMessage.content,
515
652
  roundId: round.id,
516
653
  timestamp: round.userMessage.timestamp,
654
+ ...(userAttachments ? { attachments: userAttachments } : {}),
517
655
  });
518
656
  // Assistant message(如果存在)
519
657
  if (round.assistantMessage) {