@downcity/agent 1.1.228 → 1.1.232

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 (157) 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/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  10. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +0 -7
  11. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  12. package/bin/executor/Executor.d.ts +25 -0
  13. package/bin/executor/Executor.d.ts.map +1 -1
  14. package/bin/executor/Executor.js +36 -0
  15. package/bin/executor/Executor.js.map +1 -1
  16. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +4 -0
  17. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  18. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -1
  19. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  20. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +15 -11
  21. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  22. package/bin/executor/core-engine/CoreEngineRunner.d.ts +11 -0
  23. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  24. package/bin/executor/core-engine/CoreEngineRunner.js +12 -8
  25. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  26. package/bin/executor/store/history/SessionHistoryStore.d.ts +3 -13
  27. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  28. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -1
  29. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  30. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  31. package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
  32. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  33. package/bin/index.d.ts +2 -1
  34. package/bin/index.d.ts.map +1 -1
  35. package/bin/index.js +1 -1
  36. package/bin/index.js.map +1 -1
  37. package/bin/model/CityModelAdapter.d.ts +6 -0
  38. package/bin/model/CityModelAdapter.d.ts.map +1 -1
  39. package/bin/model/CityModelAdapter.js +13 -0
  40. package/bin/model/CityModelAdapter.js.map +1 -1
  41. package/bin/plugin/core/PluginRegistry.d.ts +22 -1
  42. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  43. package/bin/plugin/core/PluginRegistry.js +46 -4
  44. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  45. package/bin/session/Session.d.ts +11 -1
  46. package/bin/session/Session.d.ts.map +1 -1
  47. package/bin/session/Session.js +76 -11
  48. package/bin/session/Session.js.map +1 -1
  49. package/bin/session/browse/Browse.d.ts +0 -8
  50. package/bin/session/browse/Browse.d.ts.map +1 -1
  51. package/bin/session/browse/Browse.js +72 -57
  52. package/bin/session/browse/Browse.js.map +1 -1
  53. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
  54. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  55. package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
  56. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  57. package/bin/session/recorder/SessionRecorder.d.ts +20 -2
  58. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  59. package/bin/session/recorder/SessionRecorder.js +91 -46
  60. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  61. package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
  62. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  63. package/bin/session/recorder/SessionRecorderCompaction.js +50 -62
  64. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  65. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
  66. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  67. package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
  68. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  69. package/bin/session/runtime/SessionPromptRuntime.d.ts +17 -1
  70. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  71. package/bin/session/runtime/SessionPromptRuntime.js +67 -8
  72. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  73. package/bin/session/services/SessionStateService.d.ts +15 -1
  74. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  75. package/bin/session/services/SessionStateService.js +60 -39
  76. package/bin/session/services/SessionStateService.js.map +1 -1
  77. package/bin/session/services/SessionTurnService.d.ts +5 -0
  78. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  79. package/bin/session/services/SessionTurnService.js +17 -3
  80. package/bin/session/services/SessionTurnService.js.map +1 -1
  81. package/bin/session/services/SessionViewService.d.ts +1 -5
  82. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  83. package/bin/session/services/SessionViewService.js +4 -34
  84. package/bin/session/services/SessionViewService.js.map +1 -1
  85. package/bin/session/storage/Paths.d.ts +3 -15
  86. package/bin/session/storage/Paths.d.ts.map +1 -1
  87. package/bin/session/storage/Paths.js +5 -21
  88. package/bin/session/storage/Paths.js.map +1 -1
  89. package/bin/types/agent/SessionTypes.d.ts +2 -24
  90. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  91. package/bin/types/executor/SessionRunContext.d.ts +21 -0
  92. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  93. package/bin/types/plugin/PluginRuntime.d.ts +25 -0
  94. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  95. package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
  96. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  97. package/bin/types/runtime/agent/AgentContext.js +29 -6
  98. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  99. package/bin/types/session/SessionConfigMutation.d.ts +68 -0
  100. package/bin/types/session/SessionConfigMutation.d.ts.map +1 -0
  101. package/bin/types/session/SessionConfigMutation.js +10 -0
  102. package/bin/types/session/SessionConfigMutation.js.map +1 -0
  103. package/bin/types/session/SessionLocalState.d.ts +8 -0
  104. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  105. package/bin/types/session/SessionMessage.d.ts +16 -12
  106. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  107. package/bin/types/session/SessionMutation.d.ts +1 -1
  108. package/bin/types/session/SessionMutation.js +1 -1
  109. package/bin/types/session/SessionOptions.d.ts +16 -0
  110. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  111. package/bin/types/session/SessionSegment.d.ts +57 -0
  112. package/bin/types/session/SessionSegment.d.ts.map +1 -0
  113. package/bin/types/session/SessionSegment.js +8 -0
  114. package/bin/types/session/SessionSegment.js.map +1 -0
  115. package/package.json +3 -3
  116. package/scripts/city-model-tool-loop.test.mjs +32 -0
  117. package/scripts/session-action-message.test.mjs +59 -13
  118. package/scripts/session-compaction-context-window.test.mjs +95 -0
  119. package/scripts/session-config-turn-boundary.test.mjs +224 -0
  120. package/scripts/session-list-title.test.mjs +21 -3
  121. package/scripts/session-prompt-runtime.test.mjs +85 -0
  122. package/scripts/session-recorder.test.mjs +340 -11
  123. package/src/agent/local/Agent.ts +34 -3
  124. package/src/agent/local/services/AgentSessions.ts +61 -0
  125. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +0 -5
  126. package/src/executor/Executor.ts +62 -0
  127. package/src/executor/composer/compaction/SessionCompactionComposer.ts +5 -0
  128. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +16 -14
  129. package/src/executor/core-engine/CoreEngineRunner.ts +24 -8
  130. package/src/executor/store/history/SessionHistoryStore.ts +3 -14
  131. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -1
  132. package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
  133. package/src/index.ts +8 -0
  134. package/src/model/CityModelAdapter.ts +15 -0
  135. package/src/plugin/core/PluginRegistry.ts +72 -4
  136. package/src/session/Session.ts +89 -11
  137. package/src/session/browse/Browse.ts +73 -69
  138. package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
  139. package/src/session/recorder/SessionRecorder.ts +116 -49
  140. package/src/session/recorder/SessionRecorderCompaction.ts +55 -80
  141. package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
  142. package/src/session/runtime/SessionPromptRuntime.ts +100 -9
  143. package/src/session/services/SessionStateService.ts +79 -44
  144. package/src/session/services/SessionTurnService.ts +19 -3
  145. package/src/session/services/SessionViewService.ts +4 -50
  146. package/src/session/storage/Paths.ts +5 -38
  147. package/src/types/agent/SessionTypes.ts +2 -24
  148. package/src/types/executor/SessionRunContext.ts +25 -0
  149. package/src/types/plugin/PluginRuntime.ts +27 -0
  150. package/src/types/runtime/agent/AgentContext.ts +32 -7
  151. package/src/types/session/SessionConfigMutation.ts +78 -0
  152. package/src/types/session/SessionLocalState.ts +9 -0
  153. package/src/types/session/SessionMessage.ts +16 -12
  154. package/src/types/session/SessionMutation.ts +1 -1
  155. package/src/types/session/SessionOptions.ts +21 -0
  156. package/src/types/session/SessionSegment.ts +62 -0
  157. package/tsconfig.tsbuildinfo +1 -1
@@ -11,6 +11,7 @@ import type { JsonObject, JsonValue } from "@/types/common/Json.js";
11
11
  import type {
12
12
  ListSessionMessagesInput,
13
13
  SessionActionMessage,
14
+ SessionAssistantFilePart,
14
15
  SessionAssistantMessage,
15
16
  SessionAssistantMessagePart,
16
17
  SessionAssistantToolPart,
@@ -24,6 +25,11 @@ import type {
24
25
  SessionMutation,
25
26
  SessionMessageMutation as SessionMessageSnapshotMutation,
26
27
  } from "@/types/session/SessionMutation.js";
28
+ import type {
29
+ SessionContextSnapshot,
30
+ SessionMessageStorageStats,
31
+ SessionSegmentSummary,
32
+ } from "@/types/session/SessionSegment.js";
27
33
 
28
34
  /** SessionRecorder 构造参数。 */
29
35
  export interface SessionRecorderOptions {
@@ -289,38 +295,88 @@ export class SessionRecorder {
289
295
  }))) as SessionErrorMessage;
290
296
  }
291
297
 
292
- /** 读取折叠后的 Message snapshot。 */
298
+ /** 读取 Active 或指定边界之前最近的完整 Segment。 */
293
299
  async list_messages(
294
300
  input?: ListSessionMessagesInput,
295
301
  ): Promise<SessionMessagePage> {
296
302
  await this.ensure_initialized();
297
- const include_internal = input?.include_internal === true;
298
- const through_sequence = Number.isFinite(input?.through_sequence)
299
- ? Number(input?.through_sequence)
300
- : Number.POSITIVE_INFINITY;
301
- const before_sequence = Number.isFinite(input?.before_sequence)
302
- ? Number(input?.before_sequence)
303
- : Number.POSITIVE_INFINITY;
304
- const all = [...this.messages_by_id.values()]
305
- .filter(
306
- (message) =>
307
- (include_internal || message.visibility === "visible") &&
308
- message.sequence <= through_sequence &&
309
- message.sequence < before_sequence,
310
- )
311
- .sort((left, right) => left.sequence - right.sequence);
312
- const offset = decode_cursor(input?.cursor);
313
- const limit = normalize_limit(input?.limit, 100, 500);
314
- const items = all.slice(offset, offset + limit).map((item) => structuredClone(item));
315
- const next_offset = offset + items.length;
303
+ const requests_segment = input?.before_sequence !== undefined;
304
+ const before_sequence = input?.before_sequence;
305
+ if (
306
+ requests_segment &&
307
+ (!Number.isInteger(before_sequence) || Number(before_sequence) <= 0)
308
+ ) {
309
+ throw new Error("before_sequence must be a positive integer");
310
+ }
311
+ const segment = requests_segment
312
+ ? await this.store.read_segment_before(Number(before_sequence))
313
+ : null;
314
+ const source = requests_segment ? "segment" as const : "active" as const;
315
+ const messages = requests_segment
316
+ ? segment?.messages || []
317
+ : [...this.messages_by_id.values()].sort(compare_message_sequence);
318
+ const start_sequence = messages[0]?.sequence;
319
+ const end_sequence = messages.at(-1)?.sequence;
320
+ const history_boundary = source === "segment"
321
+ ? segment?.range.start_sequence
322
+ : await this.store.active_before_sequence(messages);
323
+ const has_more = history_boundary !== undefined &&
324
+ await this.store.has_segment_before(history_boundary);
325
+ const stats = await this.store.stats();
326
+ const items = messages
327
+ .filter((message) => input?.include_internal === true || message.visibility === "visible")
328
+ .map((message) => structuredClone(message));
316
329
  return {
317
330
  items,
318
- total: all.length,
319
- ...(next_offset < all.length ? { next_cursor: encode_cursor(next_offset) } : {}),
320
- has_more: next_offset < all.length,
331
+ total: stats.message_count,
332
+ source,
333
+ ...(start_sequence !== undefined ? { start_sequence } : {}),
334
+ ...(end_sequence !== undefined ? { end_sequence } : {}),
335
+ ...(has_more && history_boundary !== undefined
336
+ ? { next_before_sequence: history_boundary }
337
+ : {}),
338
+ has_more,
321
339
  };
322
340
  }
323
341
 
342
+ /** 读取最新累计 Summary 与全部 Active Message,供模型上下文使用。 */
343
+ async context_snapshot(): Promise<SessionContextSnapshot> {
344
+ await this.ensure_initialized();
345
+ return {
346
+ summary: await this.store.read_latest_summary(),
347
+ messages: [...this.messages_by_id.values()]
348
+ .sort(compare_message_sequence)
349
+ .map((message) => structuredClone(message)),
350
+ };
351
+ }
352
+
353
+ /** 读取全部真实历史,供 Fork 等明确的全量复制操作使用。 */
354
+ async list_history_messages(): Promise<SessionMessage[]> {
355
+ await this.ensure_initialized();
356
+ return await this.store.list_history_messages();
357
+ }
358
+
359
+ /** 读取当前 Session 的存储统计。 */
360
+ async storage_stats(): Promise<SessionMessageStorageStats> {
361
+ await this.ensure_initialized();
362
+ return await this.store.stats();
363
+ }
364
+
365
+ /** 把 Active 前缀和累计 Summary 提交为不可变 Segment。 */
366
+ async compact_active(input: {
367
+ /** 移入 Segment 的最后一条真实 Message sequence。 */
368
+ through_sequence: number;
369
+ /** 写入 Segment footer 的累计 Summary。 */
370
+ summary: SessionSegmentSummary;
371
+ }): Promise<void> {
372
+ await this.ensure_initialized();
373
+ const result = await this.store.compact_active(input);
374
+ this.messages_by_id.clear();
375
+ for (const message of result.active_messages) {
376
+ this.messages_by_id.set(message.message_id, structuredClone(message));
377
+ }
378
+ }
379
+
324
380
  /** 向当前 Session 导入 fork 来源 Message,并重新分配全部身份和顺序。 */
325
381
  async import_messages(messages: SessionMessage[]): Promise<void> {
326
382
  const turn_ids = new Map<string, string>();
@@ -551,6 +607,7 @@ export class SessionRecorder {
551
607
  private async ensure_initialized(): Promise<void> {
552
608
  if (!this.initialized) await this.initialize();
553
609
  }
610
+
554
611
  }
555
612
 
556
613
  /** 单个 Assistant segment 的流式 writer。 */
@@ -671,10 +728,7 @@ export class SessionAssistantMessageWriter {
671
728
  return;
672
729
  }
673
730
  case "file":
674
- await this.upsert_part({
675
- part_id: `file:${generateId()}`,
676
- sequence: this.next_part_sequence(),
677
- type: "file",
731
+ await this.append_file_part({
678
732
  media_type: chunk.mediaType,
679
733
  url: chunk.url,
680
734
  });
@@ -689,6 +743,36 @@ export class SessionAssistantMessageWriter {
689
743
  await this.recorder.update_assistant_part(this.message_id, part);
690
744
  }
691
745
 
746
+ /**
747
+ * 把最终结果中的文件补入当前 Assistant,并对流式已写入文件去重。
748
+ */
749
+ async append_file_part(
750
+ input: Pick<SessionAssistantFilePart, "filename" | "media_type" | "url">,
751
+ ): Promise<void> {
752
+ const filename = String(input.filename || "").trim();
753
+ const current = this.current_message();
754
+ const existing = current.parts.find(
755
+ (part) =>
756
+ part.type === "file" &&
757
+ part.url === input.url &&
758
+ part.media_type === input.media_type,
759
+ );
760
+ if (existing?.type === "file") {
761
+ if (filename && String(existing.filename || "").trim() !== filename) {
762
+ await this.upsert_part({ ...existing, filename });
763
+ }
764
+ return;
765
+ }
766
+ await this.upsert_part({
767
+ part_id: `file:${generateId()}`,
768
+ sequence: this.next_part_sequence(),
769
+ type: "file",
770
+ media_type: input.media_type,
771
+ url: input.url,
772
+ ...(filename ? { filename } : {}),
773
+ });
774
+ }
775
+
692
776
  /** 当前实现逐 chunk 等待落盘,因此 flush 在返回时天然完成。 */
693
777
  async flush(): Promise<void> {}
694
778
 
@@ -907,28 +991,6 @@ function to_json_value(input: unknown): JsonValue {
907
991
  }
908
992
  }
909
993
 
910
- function normalize_limit(value: number | undefined, fallback: number, maximum: number): number {
911
- if (!Number.isFinite(value)) return fallback;
912
- return Math.min(maximum, Math.max(1, Math.floor(Number(value))));
913
- }
914
-
915
- function encode_cursor(offset: number): string {
916
- return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
917
- }
918
-
919
- function decode_cursor(cursor: string | undefined): number {
920
- const value = String(cursor || "").trim();
921
- if (!value) return 0;
922
- try {
923
- const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as { offset?: unknown };
924
- return typeof parsed.offset === "number" && parsed.offset >= 0
925
- ? Math.floor(parsed.offset)
926
- : 0;
927
- } catch {
928
- throw new Error("Invalid Session Message cursor");
929
- }
930
- }
931
-
932
994
  function resolve_import_id(map: Map<string, string>, source_id: string, prefix: string): string {
933
995
  const existing = map.get(source_id);
934
996
  if (existing) return existing;
@@ -936,3 +998,8 @@ function resolve_import_id(map: Map<string, string>, source_id: string, prefix:
936
998
  map.set(source_id, created);
937
999
  return created;
938
1000
  }
1001
+
1002
+ /** 按真实 Message sequence 升序排序。 */
1003
+ function compare_message_sequence(left: SessionMessage, right: SessionMessage): number {
1004
+ return left.sequence - right.sequence;
1005
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * SessionRecorder canonical 上下文压缩。
2
+ * SessionRecorder Active/Segment 上下文压缩。
3
3
  *
4
- * 压缩只追加一条 internal summary Message,不删除、不归档、不重写 Mutation 日志。
4
+ * Compact Active 前缀写入不可变 Segment,并在 footer 保存累计 Summary。
5
5
  */
6
6
 
7
7
  import { generateText } from "ai";
@@ -15,20 +15,19 @@ import {
15
15
  } from "@executor/composer/compaction/jsonl/JsonlSessionCompactionPrompts.js";
16
16
  import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
17
17
  import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
18
- import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
18
+ import type { SessionMessage } from "@/types/session/SessionMessage.js";
19
19
 
20
- /** 必要时把较早上下文追加为 canonical internal summary。 */
20
+ /** 必要时把 Active 前缀关闭为带累计 Summary 的 Segment。 */
21
21
  export async function compact_session_recorder_messages(input: {
22
22
  /** 当前 Session Recorder。 */
23
23
  recorder: SessionRecorder;
24
24
  /** 压缩策略和模型。 */
25
25
  compact_input: SessionHistoryCompactInput;
26
26
  }): Promise<{ compacted: boolean; reason?: string }> {
27
- const page = await input.recorder.list_messages({
28
- limit: 500,
29
- include_internal: true,
30
- });
31
- const context_messages = project_current_context(page.items);
27
+ const snapshot = await input.recorder.context_snapshot();
28
+ const context_messages = snapshot.messages.filter(
29
+ (message) => message.type === "user" || message.type === "assistant",
30
+ );
32
31
  if (context_messages.length < 4) {
33
32
  return { compacted: false, reason: "small_messages" };
34
33
  }
@@ -36,21 +35,24 @@ export async function compact_session_recorder_messages(input: {
36
35
  .map((message) => String(message.content || ""))
37
36
  .join("\n\n");
38
37
  const estimated_tokens = Math.ceil(
39
- `${system_text}\n${JSON.stringify(context_messages)}`.length / 3,
38
+ `${system_text}\n${snapshot.summary?.text || ""}\n${JSON.stringify(context_messages)}`.length / 3,
40
39
  );
41
40
  if (estimated_tokens <= input.compact_input.maxInputTokensApprox) {
42
41
  return { compacted: false, reason: "under_budget" };
43
42
  }
44
43
 
45
44
  const ratio = Math.max(0.1, Math.min(0.9, input.compact_input.compactRatio));
46
- const compact_count = Math.max(
47
- 2,
48
- Math.min(context_messages.length - 1, Math.floor(context_messages.length * ratio)),
45
+ const keep_last_messages = Math.max(1, input.compact_input.keepLastMessages);
46
+ const max_compact_count = context_messages.length - keep_last_messages;
47
+ if (max_compact_count < 2) {
48
+ return { compacted: false, reason: "keep_last_messages" };
49
+ }
50
+ const compact_count = Math.min(
51
+ max_compact_count,
52
+ Math.max(2, Math.floor(context_messages.length * ratio)),
49
53
  );
50
54
  const older = context_messages.slice(0, compact_count);
51
- const boundary = [...older]
52
- .reverse()
53
- .find((message) => message.type !== "assistant" || message.kind !== "summary");
55
+ const boundary = older.at(-1);
54
56
  if (!boundary) return { compacted: false, reason: "nothing_to_compact" };
55
57
 
56
58
  const action_id = `compacting:${input.recorder.session_id}:${generateId()}`;
@@ -61,19 +63,11 @@ export async function compact_session_recorder_messages(input: {
61
63
  state: "running",
62
64
  });
63
65
 
64
- const previous_summary = older.find(
65
- (message): message is SessionAssistantMessage =>
66
- message.type === "assistant" && message.kind === "summary",
67
- );
68
66
  const conversation_text = older
69
- .filter((message) => message !== previous_summary)
70
67
  .map((message) => message_to_compaction_text(message))
71
68
  .filter(Boolean)
72
- .join("\n")
73
- .slice(-24_000);
74
- const previous_summary_text = previous_summary
75
- ? assistant_text(previous_summary)
76
- : "";
69
+ .join("\n");
70
+ const previous_summary_text = String(snapshot.summary?.text || "").trim();
77
71
  const prompt = previous_summary_text
78
72
  ? build_update_session_compaction_prompt({
79
73
  previous_summary: previous_summary_text,
@@ -83,7 +77,7 @@ export async function compact_session_recorder_messages(input: {
83
77
  conversation_text: conversation_text || "(none)",
84
78
  });
85
79
 
86
- let summary = "";
80
+ let summary: string;
87
81
  try {
88
82
  const result = await generateText({
89
83
  model: input.compact_input.model,
@@ -91,74 +85,55 @@ export async function compact_session_recorder_messages(input: {
91
85
  prompt,
92
86
  });
93
87
  summary = String(result.text || "").trim();
94
- } catch {
95
- summary = "Earlier conversation was compacted because the context window was exceeded.";
88
+ } catch (error) {
89
+ await publish_action(input.compact_input.onAction, {
90
+ id: action_id,
91
+ session_id: input.recorder.session_id,
92
+ title: "Session message compaction failed",
93
+ description: error instanceof Error ? error.message : String(error),
94
+ state: "failed",
95
+ });
96
+ return { compacted: false, reason: "summary_failed" };
97
+ }
98
+ if (!summary) {
99
+ await publish_action(input.compact_input.onAction, {
100
+ id: action_id,
101
+ session_id: input.recorder.session_id,
102
+ title: "Session message compaction failed",
103
+ description: "The compaction model returned an empty Summary.",
104
+ state: "failed",
105
+ });
106
+ return { compacted: false, reason: "summary_failed" };
96
107
  }
97
108
 
98
- await input.recorder.append_completed_assistant_message({
99
- turn_id: `compact:${input.recorder.session_id}:${generateId()}`,
100
- kind: "summary",
101
- visibility: "internal",
102
- summary_through_message_id: boundary.message_id,
103
- parts: [{
104
- part_id: `summary-text:${generateId()}`,
105
- sequence: 1,
106
- type: "text",
109
+ await input.recorder.compact_active({
110
+ through_sequence: boundary.sequence,
111
+ summary: {
112
+ record_type: "summary",
113
+ session_id: input.recorder.session_id,
114
+ summary_id: `summary:${input.recorder.session_id}:${generateId()}`,
115
+ through_sequence: boundary.sequence,
107
116
  text: summary,
108
- state: "done",
109
- }],
117
+ created_at: Date.now(),
118
+ },
110
119
  });
111
120
  await publish_action(input.compact_input.onAction, {
112
121
  id: action_id,
113
122
  session_id: input.recorder.session_id,
114
123
  title: "Session messages compacted",
115
- description: `Compacted through Message ${boundary.message_id}.`,
124
+ description: `Closed Active through Message ${boundary.message_id}.`,
116
125
  state: "completed",
117
126
  });
118
127
  return { compacted: true };
119
128
  }
120
129
 
121
- function project_current_context(messages: SessionMessage[]): SessionMessage[] {
122
- const latest_summary = [...messages].reverse().find(
123
- (message): message is SessionAssistantMessage =>
124
- message.type === "assistant" &&
125
- message.kind === "summary" &&
126
- message.visibility === "internal",
127
- );
128
- if (!latest_summary?.summary_through_message_id) {
129
- return messages.filter(
130
- (message) => message.type === "user" || message.type === "assistant",
131
- );
132
- }
133
- const boundary_index = messages.findIndex(
134
- (message) => message.message_id === latest_summary.summary_through_message_id,
135
- );
136
- return [
137
- latest_summary,
138
- ...messages.slice(boundary_index + 1).filter(
139
- (message) =>
140
- message !== latest_summary &&
141
- (message.type === "user" ||
142
- message.type === "assistant" && message.kind !== "summary"),
143
- ),
144
- ];
145
- }
146
-
147
130
  function message_to_compaction_text(message: SessionMessage): string {
148
131
  const projected = to_executor_ui_message(message);
149
132
  if (!projected) return "";
150
- const text = projected.parts
151
- .flatMap((part) => part.type === "text" ? [String(part.text || "")] : [])
152
- .join("\n")
153
- .trim();
154
- return text ? `${message.type}: ${text}` : "";
155
- }
156
-
157
- function assistant_text(message: SessionAssistantMessage): string {
158
- return message.parts
159
- .flatMap((part) => part.type === "text" ? [part.text] : [])
160
- .join("\n")
161
- .trim();
133
+ return JSON.stringify({
134
+ role: projected.role,
135
+ parts: projected.parts,
136
+ });
162
137
  }
163
138
 
164
139
  async function publish_action(
@@ -168,7 +143,7 @@ async function publish_action(
168
143
  session_id: string;
169
144
  title: string;
170
145
  description?: string;
171
- state: "running" | "completed";
146
+ state: "running" | "completed" | "failed";
172
147
  },
173
148
  ): Promise<void> {
174
149
  if (!callback) return;
@@ -2,7 +2,7 @@
2
2
  * SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
3
3
  *
4
4
  * 关键点(中文)
5
- * - Recorder 的 messages.jsonl 与 assistant_message.json 共同组成持久化事实源。
5
+ * - Recorder 的 active.jsonl、segments 与 assistant_message.json 共同组成持久化事实源。
6
6
  * - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
7
7
  * - 流式草稿写入由 AssistantMessageWriter 负责,Executor 的旧接口只提供投影兼容。
8
8
  */
@@ -111,11 +111,8 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
111
111
 
112
112
  /** 从 Recorder 当前快照投影运行中的 Assistant。 */
113
113
  async read_inflight(): Promise<SessionRecordV1 | null> {
114
- const page = await this.recorder.list_messages({
115
- limit: 500,
116
- include_internal: true,
117
- });
118
- const assistant = [...page.items]
114
+ const snapshot = await this.recorder.context_snapshot();
115
+ const assistant = [...snapshot.messages]
119
116
  .reverse()
120
117
  .find(
121
118
  (message) =>
@@ -132,15 +129,32 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
132
129
 
133
130
  /** 返回 Context Composer 使用的 UIMessage 投影。 */
134
131
  async list_records(): Promise<SessionRecordV1[]> {
135
- const page = await this.recorder.list_messages({
136
- limit: 500,
137
- include_internal: true,
138
- });
139
- const messages = project_context_messages(page.items);
140
- return messages.flatMap((message) => {
132
+ const snapshot = await this.recorder.context_snapshot();
133
+ const records: SessionRecordV1[] = [];
134
+ if (snapshot.summary) {
135
+ records.push({
136
+ id: snapshot.summary.summary_id,
137
+ role: "assistant",
138
+ metadata: {
139
+ v: 1,
140
+ ts: snapshot.summary.created_at,
141
+ sessionId: this.sessionId,
142
+ source: "compact",
143
+ kind: "summary",
144
+ extra: {
145
+ visibility: "internal",
146
+ summaryThroughSequence: snapshot.summary.through_sequence,
147
+ },
148
+ },
149
+ parts: [{ type: "text", text: snapshot.summary.text }],
150
+ });
151
+ }
152
+ for (const message of snapshot.messages) {
153
+ if (message.type !== "user" && message.type !== "assistant") continue;
141
154
  const projected = to_executor_ui_message(message);
142
- return projected ? [projected] : [];
143
- });
155
+ if (projected) records.push(projected);
156
+ }
157
+ return records;
144
158
  }
145
159
 
146
160
  /** 返回 Context Message 区间。 */
@@ -158,7 +172,7 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
158
172
  return {};
159
173
  }
160
174
 
161
- /** 通过 Recorder 追加 internal summary,不重写 Mutation 历史。 */
175
+ /** 通过 Recorder Active 前缀关闭为带累计 Summary Segment。 */
162
176
  async compact(input: SessionHistoryCompactInput): Promise<{
163
177
  compacted: boolean;
164
178
  reason?: string;
@@ -237,40 +251,6 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
237
251
  }
238
252
  }
239
253
 
240
- function project_context_messages(messages: import("@/types/session/SessionMessage.js").SessionMessage[]) {
241
- const latest_summary = [...messages]
242
- .reverse()
243
- .find(
244
- (message) =>
245
- message.type === "assistant" &&
246
- message.kind === "summary" &&
247
- message.visibility === "internal",
248
- );
249
- if (
250
- !latest_summary ||
251
- latest_summary.type !== "assistant" ||
252
- !latest_summary.summary_through_message_id
253
- ) {
254
- return messages.filter(
255
- (message) => message.type === "user" || message.type === "assistant",
256
- );
257
- }
258
- const boundary_index = messages.findIndex(
259
- (message) => message.message_id === latest_summary.summary_through_message_id,
260
- );
261
- return [
262
- latest_summary,
263
- ...messages
264
- .slice(boundary_index + 1)
265
- .filter(
266
- (message) =>
267
- message !== latest_summary &&
268
- message.type === "user" ||
269
- (message.type === "assistant" && message.kind !== "summary"),
270
- ),
271
- ];
272
- }
273
-
274
254
  function infer_action_type(message_id: string): string {
275
255
  return String(message_id || "").split(":")[0] || "action";
276
256
  }