@downcity/agent 1.1.228 → 1.1.236

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 (221) hide show
  1. package/bin/agent/local/Agent.d.ts +6 -1
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +30 -3
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentSessions.d.ts +23 -0
  6. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  7. package/bin/agent/local/services/AgentSessions.js +44 -0
  8. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  9. package/bin/agent/remote/RemoteSession.d.ts +2 -0
  10. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  11. package/bin/agent/remote/RemoteSession.js +4 -0
  12. package/bin/agent/remote/RemoteSession.js.map +1 -1
  13. package/bin/agent/remote/RemoteTransport.d.ts +2 -0
  14. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  15. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +1 -0
  16. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  17. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -7
  18. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  19. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +1 -0
  20. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  21. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +3 -0
  22. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  23. package/bin/executor/Executor.d.ts +43 -0
  24. package/bin/executor/Executor.d.ts.map +1 -1
  25. package/bin/executor/Executor.js +127 -7
  26. package/bin/executor/Executor.js.map +1 -1
  27. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +10 -0
  28. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  29. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -10
  30. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  31. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +4 -34
  32. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  33. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
  34. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  35. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +7 -59
  36. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  37. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts +38 -0
  38. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -0
  39. package/bin/executor/core-engine/CoreEngineContextCompaction.js +392 -0
  40. package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -0
  41. package/bin/executor/core-engine/CoreEngineMessageState.d.ts +15 -0
  42. package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
  43. package/bin/executor/core-engine/CoreEngineMessageState.js +20 -0
  44. package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
  45. package/bin/executor/core-engine/CoreEngineRunner.d.ts +15 -0
  46. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  47. package/bin/executor/core-engine/CoreEngineRunner.js +150 -35
  48. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  49. package/bin/executor/store/history/SessionHistoryStore.d.ts +7 -23
  50. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  51. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  52. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -4
  53. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  54. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  55. package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
  56. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  57. package/bin/executor/types/SessionRun.d.ts +8 -0
  58. package/bin/executor/types/SessionRun.d.ts.map +1 -1
  59. package/bin/index.d.ts +3 -2
  60. package/bin/index.d.ts.map +1 -1
  61. package/bin/index.js +2 -2
  62. package/bin/index.js.map +1 -1
  63. package/bin/model/CityModelAdapter.d.ts +6 -0
  64. package/bin/model/CityModelAdapter.d.ts.map +1 -1
  65. package/bin/model/CityModelAdapter.js +13 -0
  66. package/bin/model/CityModelAdapter.js.map +1 -1
  67. package/bin/plugin/core/PluginLocalExecution.js +3 -3
  68. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  69. package/bin/plugin/core/PluginRegistry.d.ts +41 -2
  70. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  71. package/bin/plugin/core/PluginRegistry.js +145 -6
  72. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  73. package/bin/rpc/Client.d.ts +4 -0
  74. package/bin/rpc/Client.d.ts.map +1 -1
  75. package/bin/rpc/Client.js +11 -0
  76. package/bin/rpc/Client.js.map +1 -1
  77. package/bin/session/Session.d.ts +15 -1
  78. package/bin/session/Session.d.ts.map +1 -1
  79. package/bin/session/Session.js +83 -11
  80. package/bin/session/Session.js.map +1 -1
  81. package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
  82. package/bin/session/SessionSystemBuilder.js +3 -1
  83. package/bin/session/SessionSystemBuilder.js.map +1 -1
  84. package/bin/session/browse/Browse.d.ts +0 -8
  85. package/bin/session/browse/Browse.d.ts.map +1 -1
  86. package/bin/session/browse/Browse.js +72 -57
  87. package/bin/session/browse/Browse.js.map +1 -1
  88. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
  89. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  90. package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
  91. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  92. package/bin/session/recorder/SessionRecorder.d.ts +20 -2
  93. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  94. package/bin/session/recorder/SessionRecorder.js +91 -46
  95. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  96. package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
  97. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  98. package/bin/session/recorder/SessionRecorderCompaction.js +118 -88
  99. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  100. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
  101. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  102. package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
  103. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  104. package/bin/session/runtime/SessionPromptRuntime.d.ts +21 -1
  105. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  106. package/bin/session/runtime/SessionPromptRuntime.js +65 -8
  107. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  108. package/bin/session/services/SessionStateService.d.ts +19 -1
  109. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  110. package/bin/session/services/SessionStateService.js +70 -40
  111. package/bin/session/services/SessionStateService.js.map +1 -1
  112. package/bin/session/services/SessionTurnService.d.ts +15 -0
  113. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  114. package/bin/session/services/SessionTurnService.js +104 -7
  115. package/bin/session/services/SessionTurnService.js.map +1 -1
  116. package/bin/session/services/SessionViewService.d.ts +1 -5
  117. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  118. package/bin/session/services/SessionViewService.js +4 -34
  119. package/bin/session/services/SessionViewService.js.map +1 -1
  120. package/bin/session/storage/Paths.d.ts +3 -15
  121. package/bin/session/storage/Paths.d.ts.map +1 -1
  122. package/bin/session/storage/Paths.js +5 -21
  123. package/bin/session/storage/Paths.js.map +1 -1
  124. package/bin/types/agent/SessionActor.d.ts +8 -0
  125. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  126. package/bin/types/agent/SessionTypes.d.ts +2 -24
  127. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  128. package/bin/types/executor/SessionRunContext.d.ts +29 -0
  129. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  130. package/bin/types/plugin/PluginRuntime.d.ts +59 -2
  131. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  132. package/bin/types/plugin/PluginState.d.ts +10 -0
  133. package/bin/types/plugin/PluginState.d.ts.map +1 -1
  134. package/bin/types/rpc/RpcProtocol.d.ts +10 -0
  135. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  136. package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
  137. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  138. package/bin/types/runtime/agent/AgentContext.js +29 -6
  139. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  140. package/bin/types/session/SessionLocalState.d.ts +8 -0
  141. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  142. package/bin/types/session/SessionMessage.d.ts +16 -12
  143. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  144. package/bin/types/session/SessionMutation.d.ts +1 -1
  145. package/bin/types/session/SessionMutation.js +1 -1
  146. package/bin/types/session/SessionOptions.d.ts +16 -0
  147. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  148. package/bin/types/session/SessionQueueCommand.d.ts +68 -0
  149. package/bin/types/session/SessionQueueCommand.d.ts.map +1 -0
  150. package/bin/types/session/SessionQueueCommand.js +10 -0
  151. package/bin/types/session/SessionQueueCommand.js.map +1 -0
  152. package/bin/types/session/SessionSegment.d.ts +57 -0
  153. package/bin/types/session/SessionSegment.d.ts.map +1 -0
  154. package/bin/types/session/SessionSegment.js +8 -0
  155. package/bin/types/session/SessionSegment.js.map +1 -0
  156. package/bin/utils/logger/Logger.d.ts +7 -9
  157. package/bin/utils/logger/Logger.d.ts.map +1 -1
  158. package/bin/utils/logger/Logger.js +11 -9
  159. package/bin/utils/logger/Logger.js.map +1 -1
  160. package/package.json +3 -3
  161. package/scripts/city-model-tool-loop.test.mjs +32 -0
  162. package/scripts/core-engine-context-compaction.test.mjs +411 -0
  163. package/scripts/logger-isolation.test.mjs +55 -0
  164. package/scripts/multi-agent-plugin-isolation.test.mjs +289 -0
  165. package/scripts/plugin-tool-bridge.test.mjs +52 -0
  166. package/scripts/remote-session-reconnect.test.mjs +18 -0
  167. package/scripts/session-action-message.test.mjs +59 -13
  168. package/scripts/session-compaction-context-window.test.mjs +96 -0
  169. package/scripts/session-config-turn-boundary.test.mjs +278 -0
  170. package/scripts/session-list-title.test.mjs +21 -3
  171. package/scripts/session-prompt-runtime.test.mjs +93 -0
  172. package/scripts/session-recorder.test.mjs +335 -15
  173. package/src/agent/local/Agent.ts +34 -3
  174. package/src/agent/local/services/AgentSessions.ts +61 -0
  175. package/src/agent/remote/RemoteSession.ts +5 -0
  176. package/src/agent/remote/RemoteTransport.ts +2 -0
  177. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +15 -5
  178. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +4 -0
  179. package/src/executor/Executor.ts +161 -7
  180. package/src/executor/composer/compaction/SessionCompactionComposer.ts +12 -0
  181. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +4 -55
  182. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +8 -66
  183. package/src/executor/core-engine/CoreEngineContextCompaction.ts +477 -0
  184. package/src/executor/core-engine/CoreEngineMessageState.ts +29 -0
  185. package/src/executor/core-engine/CoreEngineRunner.ts +211 -43
  186. package/src/executor/store/history/SessionHistoryStore.ts +7 -26
  187. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -4
  188. package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
  189. package/src/executor/types/SessionRun.ts +9 -0
  190. package/src/index.ts +9 -1
  191. package/src/model/CityModelAdapter.ts +15 -0
  192. package/src/plugin/core/PluginLocalExecution.ts +3 -3
  193. package/src/plugin/core/PluginRegistry.ts +187 -6
  194. package/src/rpc/Client.ts +12 -0
  195. package/src/session/Session.ts +97 -11
  196. package/src/session/SessionSystemBuilder.ts +4 -1
  197. package/src/session/browse/Browse.ts +73 -69
  198. package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
  199. package/src/session/recorder/SessionRecorder.ts +116 -49
  200. package/src/session/recorder/SessionRecorderCompaction.ts +131 -108
  201. package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
  202. package/src/session/runtime/SessionPromptRuntime.ts +86 -10
  203. package/src/session/services/SessionStateService.ts +89 -45
  204. package/src/session/services/SessionTurnService.ts +112 -7
  205. package/src/session/services/SessionViewService.ts +4 -50
  206. package/src/session/storage/Paths.ts +5 -38
  207. package/src/types/agent/SessionActor.ts +9 -0
  208. package/src/types/agent/SessionTypes.ts +2 -24
  209. package/src/types/executor/SessionRunContext.ts +34 -0
  210. package/src/types/plugin/PluginRuntime.ts +63 -2
  211. package/src/types/plugin/PluginState.ts +10 -0
  212. package/src/types/rpc/RpcProtocol.ts +11 -0
  213. package/src/types/runtime/agent/AgentContext.ts +32 -7
  214. package/src/types/session/SessionLocalState.ts +9 -0
  215. package/src/types/session/SessionMessage.ts +16 -12
  216. package/src/types/session/SessionMutation.ts +1 -1
  217. package/src/types/session/SessionOptions.ts +21 -0
  218. package/src/types/session/SessionQueueCommand.ts +78 -0
  219. package/src/types/session/SessionSegment.ts +62 -0
  220. package/src/utils/logger/Logger.ts +10 -10
  221. package/tsconfig.tsbuildinfo +1 -1
@@ -1,8 +1,8 @@
1
1
  /**
2
- * SessionMessage 的 JSONL 快照存储。
2
+ * Session Message Active + Segment JSONL 存储。
3
3
  *
4
- * messages.jsonl 只保存完整 Message 快照;运行中的唯一 Assistant 使用
5
- * assistant_message.json 原子覆盖,避免把每个 delta 写入历史文件。
4
+ * Active 保存上次 Compact 后产生的真实 Message;Compact Active 前缀写入按
5
+ * sequence 范围命名的不可变 Segment,并在 Segment 末尾追加累计 Summary footer。
6
6
  */
7
7
 
8
8
  import fs from "fs-extra";
@@ -10,80 +10,195 @@ import { open, readFile, stat } from "node:fs/promises";
10
10
  import path from "node:path";
11
11
  import { generateId } from "@/utils/Id.js";
12
12
  import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
13
+ import type {
14
+ SessionMessageStorageStats,
15
+ SessionSegmentRange,
16
+ SessionSegmentSnapshot,
17
+ SessionSegmentSummary,
18
+ } from "@/types/session/SessionSegment.js";
13
19
 
14
- /** JSONL 快照存储构造参数。 */
20
+ /** Active JSONL 存储构造参数。 */
15
21
  export interface JsonlSessionMessageStoreOptions {
16
22
  /** 当前 Session 标识。 */
17
23
  session_id: string;
18
- /** messages.jsonl 的绝对或相对路径。 */
24
+ /** active.jsonl 的绝对或相对路径。 */
19
25
  file_path: string;
20
- /** Assistant 运行中快照路径,默认与 messages.jsonl 同目录。 */
26
+ /** Assistant 运行中快照路径,默认与 active.jsonl 同目录。 */
21
27
  assistant_message_file_path?: string;
22
28
  }
23
29
 
24
30
  /** 在写锁内计算新 Message 所需的状态。 */
25
31
  export interface SessionMessageCommitState {
26
- /** 下一条新 Message 的线性顺序。 */
32
+ /** 下一条真实 Message 的全局线性顺序。 */
27
33
  message_sequence: number;
28
- /** 当前已持久化的完整 Message 快照。 */
34
+ /** 当前 Active 与 Assistant 草稿组成的运行态 Message */
29
35
  messages: SessionMessage[];
30
36
  }
31
37
 
32
- /** 单一 JSONL 快照存储。 */
38
+ /** Compact 提交参数。 */
39
+ export interface CompactActiveMessagesInput {
40
+ /** 移入新 Segment 的最后一条真实 Message sequence。 */
41
+ through_sequence: number;
42
+ /** 写在新 Segment 文件末尾的累计 Summary。 */
43
+ summary: SessionSegmentSummary;
44
+ }
45
+
46
+ /** Compact 提交结果。 */
47
+ export interface CompactActiveMessagesResult {
48
+ /** 新创建的不可变 Segment。 */
49
+ segment: SessionSegmentSnapshot;
50
+ /** Compact 后继续留在 Active 的真实 Message。 */
51
+ active_messages: SessionMessage[];
52
+ }
53
+
54
+ const SEGMENT_FILE_PATTERN = /^(\d+)-(\d+)\.jsonl$/;
55
+ const SEQUENCE_FILE_WIDTH = 12;
56
+
57
+ /** Active + Segment JSONL 存储。 */
33
58
  export class JsonlSessionMessageStore {
34
59
  readonly session_id: string;
35
- readonly messages_file_path: string;
60
+ readonly active_file_path: string;
36
61
  readonly assistant_message_file_path: string;
62
+ readonly segments_dir_path: string;
63
+
37
64
  private readonly lock_file_path: string;
38
65
 
39
66
  constructor(options: JsonlSessionMessageStoreOptions) {
40
67
  this.session_id = String(options.session_id || "").trim();
41
- this.messages_file_path = path.resolve(options.file_path);
68
+ this.active_file_path = path.resolve(options.file_path);
42
69
  this.assistant_message_file_path = path.resolve(
43
70
  options.assistant_message_file_path ||
44
- path.join(path.dirname(this.messages_file_path), "assistant_message.json"),
71
+ path.join(path.dirname(this.active_file_path), "assistant_message.json"),
45
72
  );
46
- this.lock_file_path = `${this.messages_file_path}.lock`;
73
+ this.segments_dir_path = path.join(path.dirname(this.active_file_path), "segments");
74
+ this.lock_file_path = `${this.active_file_path}.lock`;
47
75
  if (!this.session_id) throw new Error("JsonlSessionMessageStore requires session_id");
48
76
  }
49
77
 
50
- /** 创建目录和文件,并验证已有快照。 */
78
+ /** 创建 Active 与 Segment 布局,并清理已经完成的遗留草稿。 */
51
79
  async initialize(): Promise<void> {
52
- await this.ensure_layout();
53
- const messages = await this.read_messages_unsafe();
54
- const draft = await this.read_assistant_message();
55
- if (draft && draft.session_id !== this.session_id) {
56
- throw new Error("assistant_message.json session_id mismatch");
57
- }
58
- const finalized = draft && messages.find((message) => message.message_id === draft.message_id);
59
- if (draft && finalized && finalized.revision >= draft.revision && finalized.type === "assistant" && finalized.status !== "streaming") {
60
- await fs.remove(this.assistant_message_file_path);
61
- }
80
+ await this.with_write_lock(async () => {
81
+ const ranges = await this.list_segment_ranges();
82
+ const folded_messages = await this.read_folded_active_messages_unsafe();
83
+ const latest_segment_end = ranges.at(-1)?.end_sequence || 0;
84
+ const messages = folded_messages.filter(
85
+ (message) => message.sequence > latest_segment_end,
86
+ );
87
+
88
+ // Compact 先落 Segment 再覆盖 Active。若进程在两步之间退出,重启时按
89
+ // Segment sequence 边界清除 Active 重叠前缀,保证不丢消息且上下文不重复。
90
+ if (messages.length !== folded_messages.length) {
91
+ await this.write_active_messages_unsafe(messages);
92
+ }
93
+
94
+ const draft = await this.read_assistant_message();
95
+ if (draft && draft.session_id !== this.session_id) {
96
+ throw new Error("assistant_message.json session_id mismatch");
97
+ }
98
+ const finalized = draft && messages.find(
99
+ (message) => message.message_id === draft.message_id,
100
+ );
101
+ const effective_draft = draft && finalized?.type === "assistant" &&
102
+ finalized.revision >= draft.revision &&
103
+ finalized.status !== "streaming"
104
+ ? null
105
+ : draft;
106
+ this.validate_runtime_sequence(
107
+ effective_draft ? [...messages, effective_draft] : messages,
108
+ latest_segment_end,
109
+ );
110
+ if (
111
+ draft &&
112
+ finalized?.type === "assistant" &&
113
+ finalized.revision >= draft.revision &&
114
+ finalized.status !== "streaming"
115
+ ) {
116
+ await fs.remove(this.assistant_message_file_path);
117
+ }
118
+ });
62
119
  }
63
120
 
64
- /** 读取 JSONL 中每一行完整快照,并按 Message revision 折叠。 */
121
+ /** 读取 Active 和当前 Assistant 草稿,并按 revision 折叠。 */
65
122
  async list_messages(): Promise<SessionMessage[]> {
66
123
  await this.ensure_layout();
67
- const by_id = new Map<string, SessionMessage>();
68
- const raw = await fs.readFile(this.messages_file_path, "utf8").catch(() => "");
69
- for (const [index, line] of raw.split("\n").entries()) {
70
- const value = line.trim();
71
- if (!value) continue;
72
- try {
73
- const message = JSON.parse(value) as SessionMessage;
74
- this.validate_message(message);
75
- const previous = by_id.get(message.message_id);
76
- if (!previous || message.revision > previous.revision) by_id.set(message.message_id, message);
77
- } catch (error) {
78
- throw new Error(`Invalid session message at line ${String(index + 1)}: ${error instanceof Error ? error.message : String(error)}`);
79
- }
80
- }
124
+ const by_id = new Map(
125
+ (await this.read_active_messages_unsafe()).map((message) => [message.message_id, message]),
126
+ );
81
127
  const draft = await this.read_assistant_message();
82
128
  if (draft) {
83
129
  const previous = by_id.get(draft.message_id);
84
130
  if (!previous || draft.revision > previous.revision) by_id.set(draft.message_id, draft);
85
131
  }
86
- return [...by_id.values()].sort((left, right) => left.sequence - right.sequence);
132
+ const messages = [...by_id.values()].sort(compare_message_sequence);
133
+ const ranges = await this.list_segment_ranges();
134
+ this.validate_runtime_sequence(messages, ranges.at(-1)?.end_sequence || 0);
135
+ return messages;
136
+ }
137
+
138
+ /** 读取指定 sequence 之前最近的完整 Segment。 */
139
+ async read_segment_before(before_sequence: number): Promise<SessionSegmentSnapshot | null> {
140
+ const boundary = Number(before_sequence);
141
+ if (!Number.isFinite(boundary)) return null;
142
+ const ranges = await this.list_segment_ranges();
143
+ const range = [...ranges].reverse().find((item) => item.end_sequence < boundary);
144
+ return range ? await this.read_segment(range) : null;
145
+ }
146
+
147
+ /** 读取最新已关闭 Segment 的累计 Summary。 */
148
+ async read_latest_summary(): Promise<SessionSegmentSummary | null> {
149
+ const ranges = await this.list_segment_ranges();
150
+ const latest = ranges[ranges.length - 1];
151
+ return latest ? (await this.read_segment(latest)).summary : null;
152
+ }
153
+
154
+ /** 读取全部真实历史,供 Fork 等明确需要完整复制的操作使用。 */
155
+ async list_history_messages(): Promise<SessionMessage[]> {
156
+ const ranges = await this.list_segment_ranges();
157
+ const messages: SessionMessage[] = [];
158
+ for (const range of ranges) {
159
+ messages.push(...(await this.read_segment(range)).messages);
160
+ }
161
+ messages.push(...(await this.list_messages()));
162
+ return messages.sort(compare_message_sequence);
163
+ }
164
+
165
+ /** 读取当前存储统计,不解析与当前操作无关的旧 Segment 正文。 */
166
+ async stats(): Promise<SessionMessageStorageStats> {
167
+ const ranges = await this.list_segment_ranges();
168
+ const active_messages = await this.list_messages();
169
+ const latest_range = ranges[ranges.length - 1];
170
+ const latest_message = active_messages[active_messages.length - 1] ||
171
+ (latest_range ? (await this.read_segment(latest_range)).messages.at(-1) : null) ||
172
+ null;
173
+ const paths = [this.active_file_path, ...ranges.map((range) => range.file_path)];
174
+ const sizes = await Promise.all(
175
+ paths.map(async (file_path) => await stat(file_path).then((value) => value.size).catch(() => 0)),
176
+ );
177
+ const latest_sequence = Math.max(
178
+ latest_range?.end_sequence || 0,
179
+ active_messages.reduce((max, message) => Math.max(max, message.sequence), 0),
180
+ );
181
+ return {
182
+ message_count: latest_sequence,
183
+ history_bytes: sizes.reduce((total, size) => total + size, 0),
184
+ latest_message,
185
+ };
186
+ }
187
+
188
+ /** 判断给定边界之前是否仍存在历史 Segment。 */
189
+ async has_segment_before(before_sequence: number): Promise<boolean> {
190
+ return (await this.list_segment_ranges()).some(
191
+ (range) => range.end_sequence < before_sequence,
192
+ );
193
+ }
194
+
195
+ /** 返回当前 Active 之前的读取边界。 */
196
+ async active_before_sequence(messages: SessionMessage[]): Promise<number | undefined> {
197
+ const first = messages[0];
198
+ if (first) return first.sequence;
199
+ const ranges = await this.list_segment_ranges();
200
+ const latest = ranges[ranges.length - 1];
201
+ return latest ? latest.end_sequence + 1 : undefined;
87
202
  }
88
203
 
89
204
  /** 读取当前运行中的 Assistant 草稿。 */
@@ -91,11 +206,9 @@ export class JsonlSessionMessageStore {
91
206
  try {
92
207
  const value = await fs.readJson(this.assistant_message_file_path) as SessionAssistantMessage;
93
208
  this.validate_message(value);
94
- if (value.type !== "assistant" || value.status !== "streaming") return null;
95
- return value;
209
+ return value.type === "assistant" && value.status === "streaming" ? value : null;
96
210
  } catch (error) {
97
211
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
98
- if (error instanceof SyntaxError) throw error;
99
212
  throw error;
100
213
  }
101
214
  }
@@ -116,16 +229,18 @@ export class JsonlSessionMessageStore {
116
229
  });
117
230
  }
118
231
 
119
- /** 在写锁内分配 Message sequence 并创建唯一 Assistant 草稿。 */
232
+ /** 在写锁内分配全局 sequence 并创建唯一 Assistant 草稿。 */
120
233
  async create_assistant_message(
121
234
  build_message: (state: SessionMessageCommitState) => SessionAssistantMessage,
122
235
  ): Promise<SessionAssistantMessage> {
123
236
  return await this.with_write_lock(async () => {
124
237
  const current = await this.read_assistant_message();
125
238
  if (current) throw new Error(`Assistant draft already exists: ${current.message_id}`);
126
- const messages = await this.read_messages_unsafe();
239
+ const messages = await this.read_active_messages_unsafe();
240
+ const ranges = await this.list_segment_ranges();
241
+ this.validate_runtime_sequence(messages, ranges.at(-1)?.end_sequence || 0);
127
242
  const message = build_message({
128
- message_sequence: messages.reduce((max, item) => Math.max(max, item.sequence + 1), 1),
243
+ message_sequence: await this.next_sequence_unsafe(messages),
129
244
  messages,
130
245
  });
131
246
  this.validate_message(message);
@@ -137,25 +252,30 @@ export class JsonlSessionMessageStore {
137
252
  });
138
253
  }
139
254
 
140
- /** 创建新 Message 或追加已有 Message revision 快照。 */
255
+ /** 创建新 Message 或向 Active 追加已有 Message revision */
141
256
  async append_message(
142
257
  build_message: (state: SessionMessageCommitState) => SessionMessage,
143
258
  ): Promise<SessionMessage> {
144
259
  return await this.with_write_lock(async () => {
145
- const messages = await this.read_messages_unsafe();
260
+ const messages = await this.read_active_messages_unsafe();
146
261
  const draft = await this.read_assistant_message();
147
262
  const current_messages = draft ? [...messages, draft] : messages;
263
+ const ranges = await this.list_segment_ranges();
264
+ this.validate_runtime_sequence(
265
+ current_messages,
266
+ ranges.at(-1)?.end_sequence || 0,
267
+ );
148
268
  const message = build_message({
149
- message_sequence: current_messages.reduce((max, item) => Math.max(max, item.sequence + 1), 1),
269
+ message_sequence: await this.next_sequence_unsafe(current_messages),
150
270
  messages: current_messages,
151
271
  });
152
272
  this.validate_message(message);
153
- await fs.appendFile(this.messages_file_path, `${JSON.stringify(message)}\n`, "utf8");
273
+ await fs.appendFile(this.active_file_path, `${JSON.stringify(message)}\n`, "utf8");
154
274
  return message;
155
275
  });
156
276
  }
157
277
 
158
- /** 将 Assistant 草稿最终追加为历史快照,并删除草稿文件。 */
278
+ /** 将 Assistant 草稿最终追加到 Active,并删除草稿文件。 */
159
279
  async finalize_assistant_message(message: SessionAssistantMessage): Promise<void> {
160
280
  this.validate_message(message);
161
281
  if (message.status === "streaming") throw new Error("Final Assistant message cannot be streaming");
@@ -167,13 +287,128 @@ export class JsonlSessionMessageStore {
167
287
  if (message.revision !== current.revision + 1) {
168
288
  throw new Error(`Invalid final Assistant revision: ${message.message_id}`);
169
289
  }
170
- await fs.appendFile(this.messages_file_path, `${JSON.stringify(message)}\n`, "utf8");
290
+ await fs.appendFile(this.active_file_path, `${JSON.stringify(message)}\n`, "utf8");
171
291
  await fs.remove(this.assistant_message_file_path);
172
292
  });
173
293
  }
174
294
 
175
- private async read_messages_unsafe(): Promise<SessionMessage[]> {
176
- const raw = await fs.readFile(this.messages_file_path, "utf8").catch(() => "");
295
+ /** Active 前缀与累计 Summary 提交为不可变 Segment。 */
296
+ async compact_active(
297
+ input: CompactActiveMessagesInput,
298
+ ): Promise<CompactActiveMessagesResult> {
299
+ return await this.with_write_lock(async () => {
300
+ const draft = await this.read_assistant_message();
301
+ if (draft) throw new Error("Cannot compact while Assistant is streaming");
302
+ const active_messages = await this.read_active_messages_unsafe();
303
+ const ranges = await this.list_segment_ranges();
304
+ this.validate_runtime_sequence(
305
+ active_messages,
306
+ ranges.at(-1)?.end_sequence || 0,
307
+ );
308
+ const segment_messages = active_messages.filter(
309
+ (message) => message.sequence <= input.through_sequence,
310
+ );
311
+ const retained_messages = active_messages.filter(
312
+ (message) => message.sequence > input.through_sequence,
313
+ );
314
+ if (segment_messages.length === 0) {
315
+ throw new Error("Compact boundary does not include Active messages");
316
+ }
317
+ const start_sequence = segment_messages[0].sequence;
318
+ const end_sequence = segment_messages.at(-1)?.sequence || 0;
319
+ if (input.summary.through_sequence !== end_sequence) {
320
+ throw new Error("Compact Summary boundary does not match Segment end");
321
+ }
322
+ const range: SessionSegmentRange = {
323
+ start_sequence,
324
+ end_sequence,
325
+ file_path: this.segment_file_path(start_sequence, end_sequence),
326
+ };
327
+ if (await fs.pathExists(range.file_path)) {
328
+ throw new Error(`Session Segment already exists: ${range.file_path}`);
329
+ }
330
+ await this.write_segment_unsafe(range, segment_messages, input.summary);
331
+ await this.write_active_messages_unsafe(retained_messages);
332
+ return {
333
+ segment: { range, messages: segment_messages, summary: input.summary },
334
+ active_messages: retained_messages,
335
+ };
336
+ });
337
+ }
338
+
339
+ /** 扫描 Segment 文件名并返回 sequence 升序索引。 */
340
+ async list_segment_ranges(): Promise<SessionSegmentRange[]> {
341
+ await this.ensure_layout();
342
+ const entries = await fs.readdir(this.segments_dir_path, { withFileTypes: true });
343
+ const ranges = entries
344
+ .flatMap<SessionSegmentRange>((entry) => {
345
+ if (!entry.isFile()) return [];
346
+ const match = SEGMENT_FILE_PATTERN.exec(entry.name);
347
+ if (!match) return [];
348
+ const start_sequence = Number(match[1]);
349
+ const end_sequence = Number(match[2]);
350
+ if (!Number.isInteger(start_sequence) || !Number.isInteger(end_sequence)) return [];
351
+ return [{
352
+ start_sequence,
353
+ end_sequence,
354
+ file_path: path.join(this.segments_dir_path, entry.name),
355
+ }];
356
+ })
357
+ .sort((left, right) => left.start_sequence - right.start_sequence);
358
+ for (let index = 0; index < ranges.length; index += 1) {
359
+ const range = ranges[index];
360
+ const previous = ranges[index - 1];
361
+ if (!range || range.start_sequence > range.end_sequence) {
362
+ throw new Error("Session Segment filename contains an invalid sequence range");
363
+ }
364
+ if (index === 0 && range.start_sequence !== 1) {
365
+ throw new Error("The first Session Segment must start at sequence 1");
366
+ }
367
+ if (previous && range.start_sequence !== previous.end_sequence + 1) {
368
+ throw new Error("Session Segment sequence ranges must be contiguous and non-overlapping");
369
+ }
370
+ }
371
+ return ranges;
372
+ }
373
+
374
+ /** 读取并验证一个不可变 Segment。 */
375
+ private async read_segment(range: SessionSegmentRange): Promise<SessionSegmentSnapshot> {
376
+ const raw = await fs.readFile(range.file_path, "utf8");
377
+ const lines = raw.split("\n").map((line) => line.trim()).filter(Boolean);
378
+ const summary_value = JSON.parse(lines.pop() || "null") as SessionSegmentSummary | null;
379
+ this.validate_summary(summary_value, range.end_sequence);
380
+ const messages = lines.map((line) => {
381
+ const message = JSON.parse(line) as SessionMessage;
382
+ this.validate_message(message);
383
+ return message;
384
+ }).sort(compare_message_sequence);
385
+ if (
386
+ messages.length !== range.end_sequence - range.start_sequence + 1 ||
387
+ messages[0]?.sequence !== range.start_sequence ||
388
+ messages.at(-1)?.sequence !== range.end_sequence ||
389
+ messages.some(
390
+ (message, index) => message.sequence !== range.start_sequence + index,
391
+ )
392
+ ) {
393
+ throw new Error(`Session Segment range mismatch: ${range.file_path}`);
394
+ }
395
+ return { range, messages, summary: summary_value as SessionSegmentSummary };
396
+ }
397
+
398
+ /** 读取 Active 中每个 Message 的最高 revision。 */
399
+ private async read_active_messages_unsafe(): Promise<SessionMessage[]> {
400
+ const messages = await this.read_folded_active_messages_unsafe();
401
+ const ranges = await this.list_segment_ranges();
402
+ const latest_segment_end = ranges.at(-1)?.end_sequence || 0;
403
+ const active_messages = messages.filter(
404
+ (message) => message.sequence > latest_segment_end,
405
+ );
406
+ return active_messages;
407
+ }
408
+
409
+ /** 读取 Active 原始行,并按 Message ID 保留最高 revision。 */
410
+ private async read_folded_active_messages_unsafe(): Promise<SessionMessage[]> {
411
+ const raw = await fs.readFile(this.active_file_path, "utf8").catch(() => "");
177
412
  const by_id = new Map<string, SessionMessage>();
178
413
  for (const line of raw.split("\n")) {
179
414
  if (!line.trim()) continue;
@@ -182,16 +417,58 @@ export class JsonlSessionMessageStore {
182
417
  const previous = by_id.get(message.message_id);
183
418
  if (!previous || message.revision > previous.revision) by_id.set(message.message_id, message);
184
419
  }
185
- return [...by_id.values()].sort((left, right) => left.sequence - right.sequence);
420
+ return [...by_id.values()].sort(compare_message_sequence);
421
+ }
422
+
423
+ /** 计算下一条真实 Message sequence。 */
424
+ private async next_sequence_unsafe(messages: SessionMessage[]): Promise<number> {
425
+ const ranges = await this.list_segment_ranges();
426
+ return Math.max(
427
+ ranges.at(-1)?.end_sequence || 0,
428
+ messages.reduce((max, message) => Math.max(max, message.sequence), 0),
429
+ ) + 1;
186
430
  }
187
431
 
432
+ /** 原子覆盖 Assistant 草稿。 */
188
433
  private async write_assistant_message_unsafe(message: SessionAssistantMessage): Promise<void> {
189
434
  const temporary_path = `${this.assistant_message_file_path}.${process.pid}.${Date.now()}.tmp`;
190
- await fs.ensureDir(path.dirname(this.assistant_message_file_path));
191
435
  await fs.writeJson(temporary_path, message, { spaces: 2 });
192
436
  await fs.move(temporary_path, this.assistant_message_file_path, { overwrite: true });
193
437
  }
194
438
 
439
+ /** 原子覆盖 Active,只写入每个 Message 的最终 revision。 */
440
+ private async write_active_messages_unsafe(messages: SessionMessage[]): Promise<void> {
441
+ const temporary_path = `${this.active_file_path}.${process.pid}.${Date.now()}.tmp`;
442
+ const content = messages.length > 0
443
+ ? `${messages.map((message) => JSON.stringify(message)).join("\n")}\n`
444
+ : "";
445
+ await fs.writeFile(temporary_path, content, "utf8");
446
+ await fs.move(temporary_path, this.active_file_path, { overwrite: true });
447
+ }
448
+
449
+ /** 原子创建一个带累计 Summary footer 的 Segment。 */
450
+ private async write_segment_unsafe(
451
+ range: SessionSegmentRange,
452
+ messages: SessionMessage[],
453
+ summary: SessionSegmentSummary,
454
+ ): Promise<void> {
455
+ const temporary_path = `${range.file_path}.${process.pid}.${Date.now()}.tmp`;
456
+ const content = `${[
457
+ ...messages.map((message) => JSON.stringify(message)),
458
+ JSON.stringify(summary),
459
+ ].join("\n")}\n`;
460
+ await fs.writeFile(temporary_path, content, "utf8");
461
+ await fs.move(temporary_path, range.file_path, { overwrite: false });
462
+ }
463
+
464
+ /** 生成稳定的 Segment sequence 范围文件名。 */
465
+ private segment_file_path(start_sequence: number, end_sequence: number): string {
466
+ const start = String(start_sequence).padStart(SEQUENCE_FILE_WIDTH, "0");
467
+ const end = String(end_sequence).padStart(SEQUENCE_FILE_WIDTH, "0");
468
+ return path.join(this.segments_dir_path, `${start}-${end}.jsonl`);
469
+ }
470
+
471
+ /** 验证真实 Session Message。 */
195
472
  private validate_message(message: SessionMessage): void {
196
473
  if (!message || typeof message !== "object") throw new Error("message must be an object");
197
474
  if (message.session_id !== this.session_id) throw new Error("session_id mismatch");
@@ -209,11 +486,43 @@ export class JsonlSessionMessageStore {
209
486
  }
210
487
  }
211
488
 
489
+ /** 验证 Segment footer Summary。 */
490
+ private validate_summary(
491
+ summary: SessionSegmentSummary | null,
492
+ expected_sequence: number,
493
+ ): asserts summary is SessionSegmentSummary {
494
+ if (!summary || summary.record_type !== "summary") {
495
+ throw new Error("Session Segment summary footer is required");
496
+ }
497
+ if (summary.session_id !== this.session_id || !summary.summary_id) {
498
+ throw new Error("Session Segment summary identity mismatch");
499
+ }
500
+ if (summary.through_sequence !== expected_sequence || !String(summary.text || "").trim()) {
501
+ throw new Error("Session Segment summary boundary is invalid");
502
+ }
503
+ }
504
+
505
+ /** 验证 Active 与草稿合并后紧接最新 Segment,且真实 Message sequence 连续。 */
506
+ private validate_runtime_sequence(
507
+ messages: SessionMessage[],
508
+ latest_segment_end: number,
509
+ ): void {
510
+ const ordered_messages = [...messages].sort(compare_message_sequence);
511
+ if (ordered_messages.some(
512
+ (message, index) => message.sequence !== latest_segment_end + index + 1,
513
+ )) {
514
+ throw new Error("Active and draft Session Message sequences must be contiguous");
515
+ }
516
+ }
517
+
518
+ /** 创建 Active、Segment 与锁所需目录。 */
212
519
  private async ensure_layout(): Promise<void> {
213
- await fs.ensureDir(path.dirname(this.messages_file_path));
214
- await fs.ensureFile(this.messages_file_path);
520
+ await fs.ensureDir(path.dirname(this.active_file_path));
521
+ await fs.ensureDir(this.segments_dir_path);
522
+ await fs.ensureFile(this.active_file_path);
215
523
  }
216
524
 
525
+ /** 使用同目录锁串行化 Active、草稿与 Segment 写入。 */
217
526
  private async with_write_lock<T>(callback: () => Promise<T>): Promise<T> {
218
527
  await this.ensure_layout();
219
528
  const token = `${process.pid}:${Date.now()}:${generateId()}`;
@@ -231,7 +540,9 @@ export class JsonlSessionMessageStore {
231
540
  const lock_stat = await stat(this.lock_file_path);
232
541
  if (Date.now() - lock_stat.mtimeMs > stale_ms) await fs.remove(this.lock_file_path);
233
542
  } catch { /* 锁文件可能刚被释放。 */ }
234
- if (Date.now() - started_at > stale_ms * 2) throw new Error(`Session message lock timeout: ${this.lock_file_path}`);
543
+ if (Date.now() - started_at > stale_ms * 2) {
544
+ throw new Error(`Session message lock timeout: ${this.lock_file_path}`);
545
+ }
235
546
  await new Promise((resolve) => setTimeout(resolve, 40));
236
547
  }
237
548
  }
@@ -239,8 +550,15 @@ export class JsonlSessionMessageStore {
239
550
  return await callback();
240
551
  } finally {
241
552
  try {
242
- if ((await readFile(this.lock_file_path, "utf8")).trim() === token) await fs.remove(this.lock_file_path);
553
+ if ((await readFile(this.lock_file_path, "utf8")).trim() === token) {
554
+ await fs.remove(this.lock_file_path);
555
+ }
243
556
  } catch { /* 锁已被清理。 */ }
244
557
  }
245
558
  }
246
559
  }
560
+
561
+ /** 按全局 Message sequence 升序排序。 */
562
+ function compare_message_sequence(left: SessionMessage, right: SessionMessage): number {
563
+ return left.sequence - right.sequence;
564
+ }