@jingyi0605/codingns 0.1.4 → 0.2.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 (239) hide show
  1. package/dist/public/assets/{TerminalPage-4ulgBhv9.js → TerminalPage-BlbQuWi1.js} +1 -1
  2. package/dist/public/assets/gemini-D4G1NbrE.png +0 -0
  3. package/dist/public/assets/index-1VIm8lVL.css +1 -0
  4. package/dist/public/assets/index-Dti93O2S.js +109 -0
  5. package/dist/public/assets/kimi-BWNNSh7e.png +0 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/config/env.d.ts +7 -0
  8. package/dist/server/config/env.js +150 -1
  9. package/dist/server/config/env.js.map +1 -1
  10. package/dist/server/config/opencode-system-probe-helper-process.d.ts +24 -0
  11. package/dist/server/config/opencode-system-probe-helper-process.js +70 -5
  12. package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -1
  13. package/dist/server/modules/butler/butler-action-context-service.d.ts +30 -0
  14. package/dist/server/modules/butler/butler-action-context-service.js +108 -0
  15. package/dist/server/modules/butler/butler-action-context-service.js.map +1 -0
  16. package/dist/server/modules/butler/butler-auth-service.d.ts +17 -0
  17. package/dist/server/modules/butler/butler-auth-service.js +91 -0
  18. package/dist/server/modules/butler/butler-auth-service.js.map +1 -0
  19. package/dist/server/modules/butler/butler-control-action-service.d.ts +65 -0
  20. package/dist/server/modules/butler/butler-control-action-service.js +296 -0
  21. package/dist/server/modules/butler/butler-control-action-service.js.map +1 -0
  22. package/dist/server/modules/butler/butler-control-session-service.d.ts +55 -0
  23. package/dist/server/modules/butler/butler-control-session-service.js +367 -0
  24. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -0
  25. package/dist/server/modules/butler/butler-controller.d.ts +367 -0
  26. package/dist/server/modules/butler/butler-controller.js +475 -0
  27. package/dist/server/modules/butler/butler-controller.js.map +1 -0
  28. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.d.ts +34 -0
  29. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js +77 -0
  30. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js.map +1 -0
  31. package/dist/server/modules/butler/butler-follow-up-scheduler.d.ts +23 -0
  32. package/dist/server/modules/butler/butler-follow-up-scheduler.js +57 -0
  33. package/dist/server/modules/butler/butler-follow-up-scheduler.js.map +1 -0
  34. package/dist/server/modules/butler/butler-follow-up-service.d.ts +86 -0
  35. package/dist/server/modules/butler/butler-follow-up-service.js +948 -0
  36. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -0
  37. package/dist/server/modules/butler/butler-inbox-service.d.ts +35 -0
  38. package/dist/server/modules/butler/butler-inbox-service.js +136 -0
  39. package/dist/server/modules/butler/butler-inbox-service.js.map +1 -0
  40. package/dist/server/modules/butler/butler-notification-service.d.ts +12 -0
  41. package/dist/server/modules/butler/butler-notification-service.js +45 -0
  42. package/dist/server/modules/butler/butler-notification-service.js.map +1 -0
  43. package/dist/server/modules/butler/butler-profile-service.d.ts +26 -0
  44. package/dist/server/modules/butler/butler-profile-service.js +529 -0
  45. package/dist/server/modules/butler/butler-profile-service.js.map +1 -0
  46. package/dist/server/modules/butler/butler-project-service.d.ts +48 -0
  47. package/dist/server/modules/butler/butler-project-service.js +253 -0
  48. package/dist/server/modules/butler/butler-project-service.js.map +1 -0
  49. package/dist/server/modules/butler/butler-session-service.d.ts +79 -0
  50. package/dist/server/modules/butler/butler-session-service.js +503 -0
  51. package/dist/server/modules/butler/butler-session-service.js.map +1 -0
  52. package/dist/server/modules/butler/butler-session-summary-service.d.ts +55 -0
  53. package/dist/server/modules/butler/butler-session-summary-service.js +382 -0
  54. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -0
  55. package/dist/server/modules/butler/context-aggregator.d.ts +187 -0
  56. package/dist/server/modules/butler/context-aggregator.js +807 -0
  57. package/dist/server/modules/butler/context-aggregator.js.map +1 -0
  58. package/dist/server/modules/butler/instruction-adapter.d.ts +28 -0
  59. package/dist/server/modules/butler/instruction-adapter.js +101 -0
  60. package/dist/server/modules/butler/instruction-adapter.js.map +1 -0
  61. package/dist/server/modules/butler/patrol-execution-service.d.ts +47 -0
  62. package/dist/server/modules/butler/patrol-execution-service.js +347 -0
  63. package/dist/server/modules/butler/patrol-execution-service.js.map +1 -0
  64. package/dist/server/modules/butler/patrol-plan-service.d.ts +54 -0
  65. package/dist/server/modules/butler/patrol-plan-service.js +272 -0
  66. package/dist/server/modules/butler/patrol-plan-service.js.map +1 -0
  67. package/dist/server/modules/butler/patrol-run-service.d.ts +60 -0
  68. package/dist/server/modules/butler/patrol-run-service.js +185 -0
  69. package/dist/server/modules/butler/patrol-run-service.js.map +1 -0
  70. package/dist/server/modules/butler/patrol-scheduler.d.ts +36 -0
  71. package/dist/server/modules/butler/patrol-scheduler.js +99 -0
  72. package/dist/server/modules/butler/patrol-scheduler.js.map +1 -0
  73. package/dist/server/modules/butler/project-memory-service.d.ts +30 -0
  74. package/dist/server/modules/butler/project-memory-service.js +103 -0
  75. package/dist/server/modules/butler/project-memory-service.js.map +1 -0
  76. package/dist/server/modules/butler/provider-adapter-registry.d.ts +61 -0
  77. package/dist/server/modules/butler/provider-adapter-registry.js +430 -0
  78. package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -0
  79. package/dist/server/modules/butler/session-summary-instruction-adapter.d.ts +28 -0
  80. package/dist/server/modules/butler/session-summary-instruction-adapter.js +79 -0
  81. package/dist/server/modules/butler/session-summary-instruction-adapter.js.map +1 -0
  82. package/dist/server/modules/butler/session-summary-scheduler.d.ts +23 -0
  83. package/dist/server/modules/butler/session-summary-scheduler.js +57 -0
  84. package/dist/server/modules/butler/session-summary-scheduler.js.map +1 -0
  85. package/dist/server/modules/butler/verification-run-service.d.ts +73 -0
  86. package/dist/server/modules/butler/verification-run-service.js +633 -0
  87. package/dist/server/modules/butler/verification-run-service.js.map +1 -0
  88. package/dist/server/modules/file/file-controller.d.ts +12 -1
  89. package/dist/server/modules/file/file-controller.js +72 -1
  90. package/dist/server/modules/file/file-controller.js.map +1 -1
  91. package/dist/server/modules/file/file-preview-link-service.d.ts +22 -0
  92. package/dist/server/modules/file/file-preview-link-service.js +160 -0
  93. package/dist/server/modules/file/file-preview-link-service.js.map +1 -0
  94. package/dist/server/modules/preferences/profile-service.js +8 -2
  95. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  96. package/dist/server/modules/sessions/claude-runtime-helper-process.js +1 -1
  97. package/dist/server/modules/sessions/claude-runtime-helper-process.js.map +1 -1
  98. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +7 -2
  99. package/dist/server/modules/sessions/codex-app-server-helper-client.js +113 -2
  100. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  101. package/dist/server/modules/sessions/codex-app-server-helper-process.js +106 -1
  102. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  103. package/dist/server/modules/sessions/session-controller.d.ts +24 -1
  104. package/dist/server/modules/sessions/session-controller.js +34 -3
  105. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  106. package/dist/server/modules/sessions/session-history-service.d.ts +47 -2
  107. package/dist/server/modules/sessions/session-history-service.js +881 -56
  108. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  109. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +30 -2
  110. package/dist/server/modules/sessions/session-live-runtime-service.js +584 -159
  111. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  112. package/dist/server/modules/sessions/session-provider-error-mapper.js +94 -0
  113. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  114. package/dist/server/modules/workbench/workbench-service.d.ts +7 -1
  115. package/dist/server/modules/workbench/workbench-service.js +31 -7
  116. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  117. package/dist/server/routes/butler.d.ts +3 -0
  118. package/dist/server/routes/butler.js +54 -0
  119. package/dist/server/routes/butler.js.map +1 -0
  120. package/dist/server/routes/files.js +2 -0
  121. package/dist/server/routes/files.js.map +1 -1
  122. package/dist/server/routes/sessions.js +1 -0
  123. package/dist/server/routes/sessions.js.map +1 -1
  124. package/dist/server/server/create-server.d.ts +65 -0
  125. package/dist/server/server/create-server.js +154 -5
  126. package/dist/server/server/create-server.js.map +1 -1
  127. package/dist/server/shared/utils/command-availability.d.ts +1 -0
  128. package/dist/server/shared/utils/command-availability.js +83 -0
  129. package/dist/server/shared/utils/command-availability.js.map +1 -0
  130. package/dist/server/storage/repositories/butler-control-event-repository.d.ts +8 -0
  131. package/dist/server/storage/repositories/butler-control-event-repository.js +78 -0
  132. package/dist/server/storage/repositories/butler-control-event-repository.js.map +1 -0
  133. package/dist/server/storage/repositories/butler-control-session-repository.d.ts +11 -0
  134. package/dist/server/storage/repositories/butler-control-session-repository.js +86 -0
  135. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -0
  136. package/dist/server/storage/repositories/butler-follow-up-task-repository.d.ts +16 -0
  137. package/dist/server/storage/repositories/butler-follow-up-task-repository.js +252 -0
  138. package/dist/server/storage/repositories/butler-follow-up-task-repository.js.map +1 -0
  139. package/dist/server/storage/repositories/butler-inbox-item-repository.d.ts +15 -0
  140. package/dist/server/storage/repositories/butler-inbox-item-repository.js +111 -0
  141. package/dist/server/storage/repositories/butler-inbox-item-repository.js.map +1 -0
  142. package/dist/server/storage/repositories/butler-notification-archive-repository.d.ts +9 -0
  143. package/dist/server/storage/repositories/butler-notification-archive-repository.js +48 -0
  144. package/dist/server/storage/repositories/butler-notification-archive-repository.js.map +1 -0
  145. package/dist/server/storage/repositories/butler-profile-repository.d.ts +9 -0
  146. package/dist/server/storage/repositories/butler-profile-repository.js +86 -0
  147. package/dist/server/storage/repositories/butler-profile-repository.js.map +1 -0
  148. package/dist/server/storage/repositories/butler-project-repository.d.ts +14 -0
  149. package/dist/server/storage/repositories/butler-project-repository.js +140 -0
  150. package/dist/server/storage/repositories/butler-project-repository.js.map +1 -0
  151. package/dist/server/storage/repositories/butler-session-repository.d.ts +11 -0
  152. package/dist/server/storage/repositories/butler-session-repository.js +106 -0
  153. package/dist/server/storage/repositories/butler-session-repository.js.map +1 -0
  154. package/dist/server/storage/repositories/butler-session-summary-state-repository.d.ts +8 -0
  155. package/dist/server/storage/repositories/butler-session-summary-state-repository.js +62 -0
  156. package/dist/server/storage/repositories/butler-session-summary-state-repository.js.map +1 -0
  157. package/dist/server/storage/repositories/patrol-plan-repository.d.ts +27 -0
  158. package/dist/server/storage/repositories/patrol-plan-repository.js +119 -0
  159. package/dist/server/storage/repositories/patrol-plan-repository.js.map +1 -0
  160. package/dist/server/storage/repositories/patrol-run-repository.d.ts +28 -0
  161. package/dist/server/storage/repositories/patrol-run-repository.js +121 -0
  162. package/dist/server/storage/repositories/patrol-run-repository.js.map +1 -0
  163. package/dist/server/storage/repositories/project-memory-repository.d.ts +15 -0
  164. package/dist/server/storage/repositories/project-memory-repository.js +150 -0
  165. package/dist/server/storage/repositories/project-memory-repository.js.map +1 -0
  166. package/dist/server/storage/repositories/session-checkpoint-repository.d.ts +9 -0
  167. package/dist/server/storage/repositories/session-checkpoint-repository.js +72 -0
  168. package/dist/server/storage/repositories/session-checkpoint-repository.js.map +1 -0
  169. package/dist/server/storage/repositories/session-fork-repository.d.ts +8 -0
  170. package/dist/server/storage/repositories/session-fork-repository.js +69 -0
  171. package/dist/server/storage/repositories/session-fork-repository.js.map +1 -0
  172. package/dist/server/storage/repositories/session-index-repository.js +40 -2
  173. package/dist/server/storage/repositories/session-index-repository.js.map +1 -1
  174. package/dist/server/storage/repositories/session-message-origin-repository.d.ts +10 -0
  175. package/dist/server/storage/repositories/session-message-origin-repository.js +93 -0
  176. package/dist/server/storage/repositories/session-message-origin-repository.js.map +1 -0
  177. package/dist/server/storage/repositories/verification-run-repository.d.ts +29 -0
  178. package/dist/server/storage/repositories/verification-run-repository.js +125 -0
  179. package/dist/server/storage/repositories/verification-run-repository.js.map +1 -0
  180. package/dist/server/storage/sqlite/client.js +146 -0
  181. package/dist/server/storage/sqlite/client.js.map +1 -1
  182. package/dist/server/storage/sqlite/schema.sql +354 -0
  183. package/dist/server/types/domain.d.ts +286 -2
  184. package/dist/server/ws/ws-server.d.ts +2 -1
  185. package/dist/server/ws/ws-server.js +2 -1
  186. package/dist/server/ws/ws-server.js.map +1 -1
  187. package/node_modules/@codingns/session-sync-core/dist/index.d.ts +4 -0
  188. package/node_modules/@codingns/session-sync-core/dist/index.js +4 -0
  189. package/node_modules/@codingns/session-sync-core/dist/index.js.map +1 -1
  190. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.d.ts +18 -0
  191. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js +659 -0
  192. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js.map +1 -0
  193. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.d.ts +11 -0
  194. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js +72 -0
  195. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js.map +1 -0
  196. package/node_modules/@codingns/session-sync-core/dist/patch-builder.d.ts +8 -0
  197. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js +89 -0
  198. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js.map +1 -1
  199. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +6 -1
  200. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +228 -7
  201. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  202. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +26 -1
  203. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +499 -3
  204. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  205. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +41 -0
  206. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +1175 -0
  207. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -0
  208. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +29 -0
  209. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +578 -0
  210. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -0
  211. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +5 -1
  212. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +271 -4
  213. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  214. package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
  215. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +147 -19
  216. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  217. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.d.ts +2 -0
  218. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +43 -5
  219. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  220. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +12 -0
  221. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +442 -71
  222. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  223. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.d.ts +21 -0
  224. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js +537 -0
  225. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js.map +1 -0
  226. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.d.ts +38 -0
  227. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js +911 -0
  228. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js.map +1 -0
  229. package/node_modules/@codingns/session-sync-core/dist/services.d.ts +2 -1
  230. package/node_modules/@codingns/session-sync-core/dist/services.js +55 -8
  231. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  232. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.d.ts +6 -0
  233. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js +9 -0
  234. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js.map +1 -0
  235. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +27 -0
  236. package/node_modules/@codingns/session-sync-core/package.json +8 -0
  237. package/package.json +1 -1
  238. package/dist/public/assets/index-C5lu52cQ.css +0 -1
  239. package/dist/public/assets/index-WpdUo_Vs.js +0 -108
@@ -1,15 +1,30 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { CapabilityService, ClaudeCodeAdapter, CodexAdapter, OpenCodeAdapter, ProviderRegistry, SessionSyncService } from "@codingns/session-sync-core";
2
+ import { CapabilityService, ClaudeCodeAdapter, CodexAdapter, GeminiAdapter, KimiAdapter, OpenCodeAdapter, ProviderRegistry, SessionSyncService } from "@codingns/session-sync-core";
3
3
  import { AppError } from "../../shared/errors/app-error.js";
4
+ import { hashContent } from "../../shared/utils/hash.js";
4
5
  import { createId } from "../../shared/utils/id.js";
5
6
  import { logPerformance } from "../../shared/utils/perf-log.js";
6
7
  import { nowIso } from "../../shared/utils/time.js";
8
+ import { isCommandAvailable } from "../../shared/utils/command-availability.js";
7
9
  import { inspectSessionActivity } from "./session-activity-inspector.js";
8
10
  import { SessionActivityAuthorityService } from "./session-activity-authority-service.js";
9
11
  import { mapSessionProviderError } from "./session-provider-error-mapper.js";
12
+ import { SessionForkRepository } from "../../storage/repositories/session-fork-repository.js";
10
13
  import { enrichClaudeCapabilities } from "../provider/claude-model-options.js";
11
14
  import { CodexModelOptionsService, enrichCodexCapabilities } from "../provider/codex-model-options.js";
12
15
  import { OpenCodeModelOptionsService, enrichOpenCodeCapabilities } from "../provider/opencode-model-options.js";
16
+ import { CodexAppServerHelperClient } from "./codex-app-server-helper-client.js";
17
+ const RECONSTRUCTED_FORK_TARGET_PROVIDERS = new Set(["codex", "claude-code", "opencode"]);
18
+ const FORK_RECONSTRUCTION_PAGE_SIZE = 200;
19
+ const MAX_FORK_DEPTH = 4;
20
+ const SESSION_START_DEFERRED_PROVIDERS = new Set([
21
+ "codex",
22
+ "claude-code",
23
+ "opencode",
24
+ "gemini",
25
+ "kimi"
26
+ ]);
27
+ const MUTABLE_HISTORY_TAIL_REFRESH_INTERVAL_MS = 1_200;
13
28
  export class SessionHistoryService {
14
29
  db;
15
30
  workspaceRepository;
@@ -19,18 +34,22 @@ export class SessionHistoryService {
19
34
  sessionMessageAttachmentService;
20
35
  sessionStateRepository;
21
36
  sessionStatusSnapshotRepository;
37
+ sessionMessageOriginRepository;
22
38
  providerRegistry;
23
39
  sessionSyncService;
24
40
  capabilityService;
25
41
  sessionActivityAuthorityService;
42
+ sessionForkRepository;
26
43
  claudeCodeHomeDir;
27
44
  codexModelOptionsService;
28
45
  openCodeModelOptionsService;
46
+ providerCliCommandPaths;
47
+ providerCliAvailability;
29
48
  workspaceDiscoveryStatuses = new Map();
30
49
  workspaceDiscoveryInflight = new Map();
31
50
  workspaceStateRefreshInflight = new Map();
32
51
  workspaceSessionRelations = new Map();
33
- constructor(db, workspaceRepository, sessionBindingRepository, sessionChangedFileService, sessionIndexRepository, sessionMessageAttachmentService, sessionStateRepository, sessionStatusSnapshotRepository, config, sessionActivityAuthorityService = new SessionActivityAuthorityService()) {
52
+ constructor(db, workspaceRepository, sessionBindingRepository, sessionChangedFileService, sessionIndexRepository, sessionMessageAttachmentService, sessionStateRepository, sessionStatusSnapshotRepository, config, sessionActivityAuthorityService = new SessionActivityAuthorityService(), sessionMessageOriginRepository = null, sessionForkRepository = null, adapterOverrides = {}) {
34
53
  this.db = db;
35
54
  this.workspaceRepository = workspaceRepository;
36
55
  this.sessionBindingRepository = sessionBindingRepository;
@@ -39,11 +58,33 @@ export class SessionHistoryService {
39
58
  this.sessionMessageAttachmentService = sessionMessageAttachmentService;
40
59
  this.sessionStateRepository = sessionStateRepository;
41
60
  this.sessionStatusSnapshotRepository = sessionStatusSnapshotRepository;
61
+ this.sessionMessageOriginRepository = sessionMessageOriginRepository;
42
62
  this.sessionActivityAuthorityService = sessionActivityAuthorityService;
63
+ this.sessionForkRepository = sessionForkRepository ?? new SessionForkRepository(db);
43
64
  this.claudeCodeHomeDir = config.claudeCodeHomeDir;
65
+ this.providerCliCommandPaths = {
66
+ "claude-code": process.platform === "win32" ? "claude.cmd" : "claude",
67
+ codex: config.codexCliPath,
68
+ gemini: config.geminiCliPath,
69
+ kimi: config.kimiCliPath
70
+ };
71
+ // CLI 是否可用只在 Host 启动时探测一次;后续统一读缓存,更新 CLI 后重启 Host 生效。
72
+ this.providerCliAvailability = buildProviderCliAvailabilitySnapshot(this.providerCliCommandPaths);
44
73
  this.providerRegistry = new ProviderRegistry([
45
74
  new ClaudeCodeAdapter({ homeDir: config.claudeCodeHomeDir }),
46
- new CodexAdapter({ homeDir: config.codexHomeDir }),
75
+ new CodexAdapter({
76
+ homeDir: config.codexHomeDir,
77
+ forkTransportFactory: adapterOverrides.codexForkTransportFactory
78
+ ?? createCodexForkTransportFactory(config.codexCliPath, config.codexHomeDir)
79
+ }),
80
+ new GeminiAdapter({
81
+ homeDir: config.geminiHomeDir,
82
+ commandPath: config.geminiCliPath
83
+ }),
84
+ new KimiAdapter({
85
+ homeDir: config.kimiHomeDir,
86
+ defaultModel: config.kimiDefaultModel
87
+ }),
47
88
  new OpenCodeAdapter({
48
89
  baseUrl: config.opencodeBaseUrl,
49
90
  baseUrlResolver: config.opencodeBaseUrlResolver?.resolve.bind(config.opencodeBaseUrlResolver),
@@ -98,15 +139,16 @@ export class SessionHistoryService {
98
139
  }
99
140
  async readSessionHistory(sessionId, cursor, limit, direction = "forward", userId) {
100
141
  const startedAt = Date.now();
101
- const binding = this.getBindingOrThrow(sessionId);
102
- const current = this.sessionStatusSnapshotRepository.findBySessionId(sessionId);
142
+ const resolvedSessionId = this.resolveCanonicalSessionId(sessionId, userId);
143
+ const binding = this.getBindingOrThrow(resolvedSessionId);
144
+ const current = this.sessionStatusSnapshotRepository.findBySessionId(resolvedSessionId);
103
145
  const safeLimit = clampLimit(limit);
104
146
  const knownTotalMessageCount = direction === "backward" && cursor === null
105
- ? this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.messageCount ?? null
147
+ ? this.sessionIndexRepository.findIndexRecordBySessionId(resolvedSessionId)?.messageCount ?? null
106
148
  : null;
107
149
  let readDurationMs = 0;
108
150
  let refreshStateDurationMs = 0;
109
- this.upsertSnapshot(sessionId, {
151
+ this.upsertSnapshot(resolvedSessionId, {
110
152
  syncStatus: "syncing",
111
153
  syncCursor: current?.syncCursor ?? cursor,
112
154
  lastSyncAt: current?.lastSyncAt ?? null,
@@ -116,9 +158,9 @@ export class SessionHistoryService {
116
158
  });
117
159
  try {
118
160
  const readStartedAt = Date.now();
119
- const page = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, cursor, safeLimit, direction, knownTotalMessageCount);
161
+ const page = await this.readPage(resolvedSessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, cursor, safeLimit, direction, knownTotalMessageCount);
120
162
  readDurationMs = Date.now() - readStartedAt;
121
- this.upsertSnapshot(sessionId, {
163
+ this.upsertSnapshot(resolvedSessionId, {
122
164
  syncStatus: "idle",
123
165
  syncCursor: direction === "backward" && cursor !== null
124
166
  ? current?.syncCursor ?? page.cursor
@@ -129,7 +171,8 @@ export class SessionHistoryService {
129
171
  resumedAt: current?.resumedAt ?? null
130
172
  });
131
173
  logPerformance("session.read_history", Date.now() - startedAt, {
132
- sessionId,
174
+ sessionId: resolvedSessionId,
175
+ requestedSessionId: sessionId,
133
176
  provider: binding.provider,
134
177
  direction,
135
178
  limit: safeLimit,
@@ -145,7 +188,8 @@ export class SessionHistoryService {
145
188
  }
146
189
  catch (error) {
147
190
  logPerformance("session.read_history.failed", Date.now() - startedAt, {
148
- sessionId,
191
+ sessionId: resolvedSessionId,
192
+ requestedSessionId: sessionId,
149
193
  provider: binding.provider,
150
194
  direction,
151
195
  limit: safeLimit,
@@ -157,10 +201,17 @@ export class SessionHistoryService {
157
201
  thresholdMs: 0,
158
202
  force: true
159
203
  });
160
- this.markSessionError(sessionId, "PROVIDER_READ_FAILED", error);
204
+ this.markSessionError(resolvedSessionId, "PROVIDER_READ_FAILED", error);
161
205
  throw mapSessionProviderError(error);
162
206
  }
163
207
  }
208
+ resolveMessageOrigin(sessionId, message) {
209
+ return this.resolveMessageOrigins(sessionId, [message])[0] ?? {
210
+ ...message,
211
+ origin: null,
212
+ originRef: null
213
+ };
214
+ }
164
215
  async findLatestUserMessage(sessionId, content, maxAttempts = 12, minTimestamp = null) {
165
216
  const binding = this.getBindingOrThrow(sessionId);
166
217
  const acceptedContents = new Set((Array.isArray(content) ? content : [content]).filter((value) => value.trim().length > 0));
@@ -171,7 +222,7 @@ export class SessionHistoryService {
171
222
  .reverse()
172
223
  .find((message) => message.role === "user" &&
173
224
  acceptedContents.has(message.content) &&
174
- isMessageAtOrAfter(message.timestamp, minTimestamp));
225
+ isAcceptedUserMessageTimestamp(binding.provider, message.timestamp, minTimestamp));
175
226
  if (matched) {
176
227
  return matched;
177
228
  }
@@ -209,11 +260,13 @@ export class SessionHistoryService {
209
260
  return this.sessionChangedFileService.listBySessionId(sessionId);
210
261
  }
211
262
  listWorkspaceSessions(workspaceId, userId) {
212
- return this.enrichSessionItems(workspaceId, this.sessionIndexRepository.listByWorkspace(workspaceId, userId));
263
+ return this.enrichSessionItems(workspaceId, this.sessionIndexRepository
264
+ .listByWorkspace(workspaceId, userId)
265
+ .filter((item) => !this.isPendingSessionAlias(item)));
213
266
  }
214
267
  getProviderCapabilitiesSnapshot(provider) {
215
268
  try {
216
- return this.capabilityService.getProviderCapabilities(provider);
269
+ return this.applyProviderCliAvailability(this.capabilityService.getProviderCapabilities(provider));
217
270
  }
218
271
  catch (error) {
219
272
  throw mapSessionProviderError(error);
@@ -222,7 +275,7 @@ export class SessionHistoryService {
222
275
  async getProviderCapabilities(provider, workspaceId) {
223
276
  try {
224
277
  const workspacePath = workspaceId ? this.getWorkspaceOrThrow(workspaceId).path : null;
225
- return await this.enrichProviderCapabilities(this.capabilityService.getProviderCapabilities(provider), workspacePath);
278
+ return await this.enrichProviderCapabilities(this.applyProviderCliAvailability(this.capabilityService.getProviderCapabilities(provider)), workspacePath);
226
279
  }
227
280
  catch (error) {
228
281
  throw mapSessionProviderError(error);
@@ -233,7 +286,7 @@ export class SessionHistoryService {
233
286
  const workspace = this.getWorkspaceOrThrow(binding.workspaceId);
234
287
  return this.capabilityService
235
288
  .getSessionCapabilities(binding.provider, binding.providerSessionId)
236
- .then((capabilities) => this.enrichProviderCapabilities(capabilities, workspace.path))
289
+ .then((capabilities) => this.enrichProviderCapabilities(this.applyProviderCliAvailability(capabilities), workspace.path))
237
290
  .catch((error) => {
238
291
  throw mapSessionProviderError(error);
239
292
  });
@@ -246,6 +299,41 @@ export class SessionHistoryService {
246
299
  const codexEnriched = await enrichCodexCapabilities(claudeEnriched, this.codexModelOptionsService);
247
300
  return enrichOpenCodeCapabilities(codexEnriched, this.openCodeModelOptionsService, workspacePath);
248
301
  }
302
+ applyProviderCliAvailability(capabilities) {
303
+ if (!isProviderCliBacked(capabilities.provider)) {
304
+ return capabilities;
305
+ }
306
+ if (this.providerCliAvailability[capabilities.provider]) {
307
+ return capabilities;
308
+ }
309
+ const limitation = buildProviderCliUnavailableMessage(capabilities.provider);
310
+ const limitations = capabilities.limitations.includes(limitation)
311
+ ? capabilities.limitations
312
+ : [limitation, ...capabilities.limitations];
313
+ return {
314
+ ...capabilities,
315
+ canStartSession: false,
316
+ canResumeSession: false,
317
+ canSendMessage: false,
318
+ supportsSubagents: false,
319
+ supportsInterrupt: false,
320
+ supportsSessionFork: false,
321
+ supportsNativeAgents: false,
322
+ limitations
323
+ };
324
+ }
325
+ assertProviderCapabilityEnabled(provider, capability, fallbackDetail) {
326
+ const capabilities = this.getProviderCapabilitiesSnapshot(provider);
327
+ if (capabilities[capability]) {
328
+ return;
329
+ }
330
+ throw new AppError({
331
+ statusCode: 409,
332
+ errorCode: "PROVIDER_UNAVAILABLE",
333
+ detail: capabilities.limitations[0] ?? fallbackDetail,
334
+ field: "provider"
335
+ });
336
+ }
249
337
  async getSessionContextUsage(sessionId) {
250
338
  const binding = this.getBindingOrThrow(sessionId);
251
339
  try {
@@ -257,6 +345,7 @@ export class SessionHistoryService {
257
345
  }
258
346
  async resumeSession(sessionId) {
259
347
  const binding = this.getBindingOrThrow(sessionId);
348
+ this.assertProviderCapabilityEnabled(binding.provider, "canResumeSession", "当前 provider 不支持继续会话");
260
349
  try {
261
350
  const result = await this.sessionSyncService.resumeSession(binding.provider, binding.providerSessionId, binding.rawStoreRef);
262
351
  this.upsertSnapshot(sessionId, {
@@ -280,8 +369,7 @@ export class SessionHistoryService {
280
369
  }
281
370
  }
282
371
  async startSession(input) {
283
- const workspace = this.getWorkspaceOrThrow(input.workspaceId);
284
- if (input.provider === "codex" || input.provider === "claude-code" || input.provider === "opencode") {
372
+ if (SESSION_START_DEFERRED_PROVIDERS.has(input.provider)) {
285
373
  throw new AppError({
286
374
  statusCode: 409,
287
375
  errorCode: "SESSION_START_DEFERRED",
@@ -289,6 +377,11 @@ export class SessionHistoryService {
289
377
  field: "provider"
290
378
  });
291
379
  }
380
+ return this.startSessionDirect(input);
381
+ }
382
+ async startSessionDirect(input) {
383
+ const workspace = this.getWorkspaceOrThrow(input.workspaceId);
384
+ this.assertProviderCapabilityEnabled(input.provider, "canStartSession", "当前 provider 不支持创建会话");
292
385
  try {
293
386
  const result = await this.sessionSyncService.startSession(input.provider, workspace.path, {
294
387
  initialPrompt: input.initialPrompt
@@ -309,7 +402,10 @@ export class SessionHistoryService {
309
402
  sessionId,
310
403
  workspaceId: workspace.id,
311
404
  provider: result.session.provider,
312
- parentSessionId: result.session.parentProviderSessionId ?? null,
405
+ parentSessionId: input.parentSessionId ?? result.session.parentProviderSessionId ?? null,
406
+ sessionKind: input.sessionKind ?? "default",
407
+ annotationSourceMessageId: input.annotationSourceMessageId ?? null,
408
+ annotationSourceText: input.annotationSourceText ?? null,
313
409
  isSubagent: result.session.isSubagent ?? false,
314
410
  subagentLabel: result.session.subagentLabel ?? null,
315
411
  title: result.session.title,
@@ -348,6 +444,236 @@ export class SessionHistoryService {
348
444
  throw mapSessionProviderError(error);
349
445
  }
350
446
  }
447
+ async forkSession(input) {
448
+ const binding = this.getBindingOrThrow(input.sessionId);
449
+ const workspace = this.getWorkspaceOrThrow(binding.workspaceId);
450
+ const targetProvider = input.targetProvider?.trim() || binding.provider;
451
+ this.assertProviderCapabilityEnabled(targetProvider, "canStartSession", "当前 provider 不支持 fork 创建会话");
452
+ const sourceMessageId = input.sourceType === "message"
453
+ ? input.sourceMessageId?.trim() || null
454
+ : null;
455
+ if (input.sourceType === "message" && !sourceMessageId) {
456
+ throw new AppError({
457
+ statusCode: 400,
458
+ errorCode: "INVALID_INPUT",
459
+ detail: "按消息派生会话时必须提供 sourceMessageId",
460
+ field: "sourceMessageId"
461
+ });
462
+ }
463
+ this.assertForkDepthWithinLimit(input.sessionId);
464
+ if (targetProvider !== binding.provider) {
465
+ return this.forkSessionAcrossProviders({
466
+ ...input,
467
+ targetProvider
468
+ }, binding, sourceMessageId);
469
+ }
470
+ try {
471
+ const result = await this.sessionSyncService.forkSession(binding.provider, binding.providerSessionId, workspace.path, {
472
+ rawStoreRef: binding.rawStoreRef,
473
+ sourceType: input.sourceType,
474
+ sourceMessageId,
475
+ strategy: input.strategy ?? "auto"
476
+ });
477
+ const sessionId = createId();
478
+ const timestamp = nowIso();
479
+ this.db.transaction(() => {
480
+ this.sessionBindingRepository.upsert({
481
+ sessionId,
482
+ workspaceId: workspace.id,
483
+ provider: result.session.provider,
484
+ providerSessionId: result.session.providerSessionId,
485
+ rawStoreRef: result.session.rawStoreRef,
486
+ createdAt: timestamp,
487
+ updatedAt: timestamp
488
+ });
489
+ this.sessionIndexRepository.upsert({
490
+ sessionId,
491
+ workspaceId: workspace.id,
492
+ provider: result.session.provider,
493
+ parentSessionId: input.sessionId,
494
+ sessionKind: input.sessionKind ?? "default",
495
+ annotationSourceMessageId: input.annotationSourceMessageId ?? null,
496
+ annotationSourceText: input.annotationSourceText ?? null,
497
+ isSubagent: result.session.isSubagent ?? false,
498
+ subagentLabel: result.session.subagentLabel ?? null,
499
+ title: result.session.title,
500
+ messageCount: result.session.messageCount,
501
+ isArchived: result.session.isArchived ?? false,
502
+ lastMessageAt: result.session.lastMessageAt,
503
+ createdAt: timestamp,
504
+ updatedAt: timestamp
505
+ });
506
+ this.sessionForkRepository.upsert({
507
+ sessionId,
508
+ parentSessionId: input.sessionId,
509
+ provider: result.session.provider,
510
+ forkSourceType: result.forkSourceType,
511
+ forkSourceSessionId: input.sessionId,
512
+ forkSourceMessageId: sourceMessageId,
513
+ inheritedPrefixMessageCount: result.inheritedPrefixMessageCount,
514
+ providerParentSessionId: binding.providerSessionId,
515
+ providerSourceMessageId: result.providerSourceMessageId ?? null,
516
+ forkMethod: result.forkMethod,
517
+ createdAt: timestamp
518
+ });
519
+ this.sessionStatusSnapshotRepository.upsert({
520
+ sessionId,
521
+ syncStatus: "idle",
522
+ syncCursor: null,
523
+ lastSyncAt: timestamp,
524
+ lastErrorCode: null,
525
+ lastErrorDetail: null,
526
+ resumedAt: null,
527
+ updatedAt: timestamp
528
+ });
529
+ this.sessionStateRepository.upsert({
530
+ sessionId,
531
+ userId: input.userId,
532
+ runningState: "idle",
533
+ activitySource: "none",
534
+ favorite: false,
535
+ lastEventAt: result.session.lastMessageAt,
536
+ completedAt: null,
537
+ lastSeenAt: null,
538
+ updatedAt: timestamp
539
+ });
540
+ })();
541
+ const forkedSession = this.getSessionListItemOrThrow(sessionId, input.userId);
542
+ const relationMap = this.workspaceSessionRelations.get(workspace.id)
543
+ ?? new Map();
544
+ relationMap.set(sessionId, {
545
+ parentSessionId: input.sessionId,
546
+ sessionKind: forkedSession.sessionKind ?? input.sessionKind ?? "default",
547
+ annotationSourceMessageId: forkedSession.annotationSourceMessageId ?? input.annotationSourceMessageId ?? null,
548
+ annotationSourceText: forkedSession.annotationSourceText ?? input.annotationSourceText ?? null,
549
+ isSubagent: forkedSession.isSubagent ?? false,
550
+ subagentLabel: forkedSession.subagentLabel ?? null
551
+ });
552
+ this.workspaceSessionRelations.set(workspace.id, relationMap);
553
+ return this.getSessionListItemOrThrow(sessionId, input.userId);
554
+ }
555
+ catch (error) {
556
+ throw mapSessionProviderError(error);
557
+ }
558
+ }
559
+ async forkSessionAcrossProviders(input, sourceBinding, sourceMessageId) {
560
+ if (!RECONSTRUCTED_FORK_TARGET_PROVIDERS.has(input.targetProvider)) {
561
+ throw mapSessionProviderError(new Error("FORK_TARGET_PROVIDER_NOT_SUPPORTED"));
562
+ }
563
+ const sourceIndex = this.sessionIndexRepository.findIndexRecordBySessionId(input.sessionId);
564
+ const inheritedMessages = await this.readForkSourceMessages(input.sessionId, sourceBinding, input.sourceType, sourceMessageId);
565
+ const reconstructedMessages = inheritedMessages.filter((message) => (message.role === "user" || message.role === "assistant")
566
+ && message.kind === "text"
567
+ && message.content.trim().length > 0);
568
+ const inheritedPrompt = buildReconstructedForkPrompt({
569
+ sourceProvider: sourceBinding.provider,
570
+ targetProvider: input.targetProvider,
571
+ sourceType: input.sourceType,
572
+ sourceTitle: sourceIndex?.title?.trim() || null,
573
+ messages: reconstructedMessages
574
+ });
575
+ const startedSession = await this.startSessionDirect({
576
+ workspaceId: sourceBinding.workspaceId,
577
+ userId: input.userId,
578
+ provider: input.targetProvider,
579
+ initialPrompt: inheritedPrompt,
580
+ parentSessionId: input.sessionId,
581
+ sessionKind: input.sessionKind ?? "default",
582
+ annotationSourceMessageId: input.annotationSourceMessageId ?? null,
583
+ annotationSourceText: input.annotationSourceText ?? null
584
+ });
585
+ const timestamp = nowIso();
586
+ const currentIndex = this.sessionIndexRepository.findIndexRecordBySessionId(startedSession.sessionId);
587
+ this.db.transaction(() => {
588
+ if (currentIndex) {
589
+ this.sessionIndexRepository.upsert({
590
+ ...currentIndex,
591
+ parentSessionId: input.sessionId,
592
+ sessionKind: input.sessionKind ?? currentIndex.sessionKind ?? "default",
593
+ annotationSourceMessageId: input.annotationSourceMessageId ?? currentIndex.annotationSourceMessageId ?? null,
594
+ annotationSourceText: input.annotationSourceText ?? currentIndex.annotationSourceText ?? null,
595
+ updatedAt: timestamp
596
+ });
597
+ }
598
+ this.sessionForkRepository.upsert({
599
+ sessionId: startedSession.sessionId,
600
+ parentSessionId: input.sessionId,
601
+ provider: input.targetProvider,
602
+ forkSourceType: input.sourceType,
603
+ forkSourceSessionId: input.sessionId,
604
+ forkSourceMessageId: sourceMessageId,
605
+ inheritedPrefixMessageCount: reconstructedMessages.length,
606
+ providerParentSessionId: sourceBinding.providerSessionId,
607
+ providerSourceMessageId: null,
608
+ forkMethod: input.sourceType === "session"
609
+ ? "reconstructed_session_fork"
610
+ : "reconstructed_message_fork",
611
+ createdAt: timestamp
612
+ });
613
+ })();
614
+ const relationMap = this.workspaceSessionRelations.get(sourceBinding.workspaceId)
615
+ ?? new Map();
616
+ relationMap.set(startedSession.sessionId, {
617
+ parentSessionId: input.sessionId,
618
+ sessionKind: startedSession.sessionKind ?? input.sessionKind ?? "default",
619
+ annotationSourceMessageId: startedSession.annotationSourceMessageId ?? input.annotationSourceMessageId ?? null,
620
+ annotationSourceText: startedSession.annotationSourceText ?? input.annotationSourceText ?? null,
621
+ isSubagent: startedSession.isSubagent ?? false,
622
+ subagentLabel: startedSession.subagentLabel ?? null
623
+ });
624
+ this.workspaceSessionRelations.set(sourceBinding.workspaceId, relationMap);
625
+ return this.getSessionListItemOrThrow(startedSession.sessionId, input.userId);
626
+ }
627
+ async readForkSourceMessages(sessionId, binding, sourceType, sourceMessageId) {
628
+ const messages = [];
629
+ let cursor = null;
630
+ while (true) {
631
+ const page = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, cursor, FORK_RECONSTRUCTION_PAGE_SIZE, "forward");
632
+ messages.push(...page.messages);
633
+ if (!page.nextCursor) {
634
+ break;
635
+ }
636
+ cursor = page.nextCursor;
637
+ }
638
+ if (sourceType === "session") {
639
+ return messages;
640
+ }
641
+ const targetIndex = messages.findIndex((message) => message.messageId === sourceMessageId);
642
+ if (targetIndex < 0) {
643
+ throw mapSessionProviderError(new Error("FORK_SOURCE_MESSAGE_NOT_FOUND"));
644
+ }
645
+ return messages.slice(0, targetIndex + 1);
646
+ }
647
+ assertForkDepthWithinLimit(parentSessionId) {
648
+ const nextDepth = this.getSessionForkDepth(parentSessionId) + 1;
649
+ if (nextDepth > MAX_FORK_DEPTH) {
650
+ throw new AppError({
651
+ statusCode: 409,
652
+ errorCode: "FORK_DEPTH_LIMIT_EXCEEDED",
653
+ detail: `fork 会话层级最多支持 ${MAX_FORK_DEPTH} 级`
654
+ });
655
+ }
656
+ }
657
+ getSessionForkDepth(sessionId) {
658
+ let depth = 1;
659
+ let currentSessionId = sessionId;
660
+ const visitedSessionIds = new Set();
661
+ while (currentSessionId) {
662
+ if (visitedSessionIds.has(currentSessionId)) {
663
+ return depth;
664
+ }
665
+ visitedSessionIds.add(currentSessionId);
666
+ const parentSessionId = this.sessionForkRepository.findBySessionId(currentSessionId)?.parentSessionId
667
+ ?? this.sessionIndexRepository.findIndexRecordBySessionId(currentSessionId)?.parentSessionId
668
+ ?? null;
669
+ if (!parentSessionId) {
670
+ return depth;
671
+ }
672
+ depth += 1;
673
+ currentSessionId = parentSessionId;
674
+ }
675
+ return depth;
676
+ }
351
677
  async sendMessage(sessionId, content, clientRequestId, permissionMode = null) {
352
678
  const binding = this.getBindingOrThrow(sessionId);
353
679
  const result = await this.sessionSyncService
@@ -357,14 +683,21 @@ export class SessionHistoryService {
357
683
  throw mapSessionProviderError(error);
358
684
  });
359
685
  const existing = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
686
+ const sessionFork = this.sessionForkRepository.findBySessionId(sessionId);
687
+ const parentTitle = sessionFork?.parentSessionId
688
+ ? this.sessionIndexRepository.findIndexRecordBySessionId(sessionFork.parentSessionId)?.title ?? null
689
+ : null;
360
690
  this.sessionIndexRepository.upsert({
361
691
  sessionId,
362
692
  workspaceId: binding.workspaceId,
363
693
  provider: binding.provider,
364
694
  parentSessionId: existing?.parentSessionId ?? null,
695
+ sessionKind: existing?.sessionKind ?? "default",
696
+ annotationSourceMessageId: existing?.annotationSourceMessageId ?? null,
697
+ annotationSourceText: existing?.annotationSourceText ?? null,
365
698
  isSubagent: existing?.isSubagent ?? false,
366
699
  subagentLabel: existing?.subagentLabel ?? null,
367
- title: existing?.title ?? result.message.content.slice(0, 48),
700
+ title: resolveSessionListTitle(binding.provider, existing?.title ?? null, result.message.content, parentTitle),
368
701
  messageCount: (existing?.messageCount ?? 0) + 1,
369
702
  isArchived: existing?.isArchived ?? false,
370
703
  lastMessageAt: result.message.timestamp,
@@ -385,7 +718,7 @@ export class SessionHistoryService {
385
718
  };
386
719
  }
387
720
  async subscribeSession(sessionId, cursor, limit, onEnvelope) {
388
- const sentMessageIds = new Set();
721
+ const deliveredMessages = createDeliveredHistoryMessageState();
389
722
  const safeLimit = clampLimit(limit);
390
723
  let currentCursor = cursor;
391
724
  const current = this.sessionStatusSnapshotRepository.findBySessionId(sessionId);
@@ -401,10 +734,10 @@ export class SessionHistoryService {
401
734
  });
402
735
  try {
403
736
  if (currentCursor === null) {
404
- currentCursor = await this.pullRecentSessionHistory(sessionId, safeLimit, sentMessageIds, onEnvelope, "session.backfill");
737
+ currentCursor = await this.pullRecentSessionHistory(sessionId, safeLimit, deliveredMessages, onEnvelope, "session.backfill");
405
738
  }
406
739
  else {
407
- await this.pullSessionHistory(sessionId, currentCursor, safeLimit, sentMessageIds, onEnvelope, "session.backfill").then((nextCursor) => {
740
+ await this.pullSessionHistory(sessionId, currentCursor, safeLimit, deliveredMessages, onEnvelope, "session.backfill").then((nextCursor) => {
408
741
  currentCursor = nextCursor;
409
742
  });
410
743
  }
@@ -418,7 +751,7 @@ export class SessionHistoryService {
418
751
  return;
419
752
  }
420
753
  polling = true;
421
- void this.pullSessionHistory(sessionId, currentCursor, safeLimit, sentMessageIds, onEnvelope, "session.delta", () => closed)
754
+ void this.pullSessionHistory(sessionId, currentCursor, safeLimit, deliveredMessages, onEnvelope, "session.delta", () => closed)
422
755
  .then((nextCursor) => {
423
756
  currentCursor = nextCursor;
424
757
  })
@@ -462,6 +795,25 @@ export class SessionHistoryService {
462
795
  messages: page.messages
463
796
  };
464
797
  }
798
+ async readAllTextHistoryMessages(sessionId, limit = FORK_RECONSTRUCTION_PAGE_SIZE) {
799
+ const binding = this.getBindingOrThrow(sessionId);
800
+ const messages = [];
801
+ let cursor = null;
802
+ let remaining = Math.max(limit, 0);
803
+ while (remaining > 0) {
804
+ const pageSize = Math.min(remaining, FORK_RECONSTRUCTION_PAGE_SIZE);
805
+ const page = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, cursor, pageSize, "forward");
806
+ messages.push(...page.messages.filter((message) => (message.role === "user" || message.role === "assistant")
807
+ && message.kind === "text"
808
+ && message.content.trim().length > 0));
809
+ if (!page.nextCursor || page.messages.length === 0) {
810
+ break;
811
+ }
812
+ cursor = page.nextCursor;
813
+ remaining -= page.messages.length;
814
+ }
815
+ return messages;
816
+ }
465
817
  async markSessionSeen(sessionId, userId) {
466
818
  const existing = this.sessionStateRepository.findBySessionAndUser(sessionId, userId) ??
467
819
  (await this.refreshSessionState(sessionId, userId));
@@ -525,6 +877,9 @@ export class SessionHistoryService {
525
877
  workspaceId: existing.workspaceId,
526
878
  provider: existing.provider,
527
879
  parentSessionId: existing.parentSessionId ?? null,
880
+ sessionKind: existing.sessionKind ?? "default",
881
+ annotationSourceMessageId: existing.annotationSourceMessageId ?? null,
882
+ annotationSourceText: existing.annotationSourceText ?? null,
528
883
  isSubagent: existing.isSubagent ?? false,
529
884
  subagentLabel: existing.subagentLabel ?? null,
530
885
  title: existing.title,
@@ -567,7 +922,7 @@ export class SessionHistoryService {
567
922
  detail: "session 不存在"
568
923
  });
569
924
  }
570
- return binding;
925
+ return this.resolvePendingSessionAliasBinding(binding) ?? binding;
571
926
  }
572
927
  persistSessionBinding(sessionId, workspaceId, snapshot) {
573
928
  if (!snapshot.providerSessionId || !snapshot.rawStoreRef) {
@@ -643,7 +998,8 @@ export class SessionHistoryService {
643
998
  continue;
644
999
  }
645
1000
  const pendingDuplicate = exactExisting
646
- ?? findClaudePendingDiscoveryDuplicate(session, existingWorkspaceSessions, claimedPendingSessionIds);
1001
+ ?? findClaudePendingDiscoveryDuplicate(session, existingWorkspaceSessions, claimedPendingSessionIds)
1002
+ ?? findKimiRuntimeDiscoveryDuplicate(session, existingWorkspaceSessions, claimedPendingSessionIds);
647
1003
  const existing = exactExisting ?? (pendingDuplicate
648
1004
  ? this.sessionBindingRepository.findBySessionId(pendingDuplicate.sessionId)
649
1005
  : null);
@@ -667,11 +1023,24 @@ export class SessionHistoryService {
667
1023
  createdAt,
668
1024
  updatedAt: timestamp
669
1025
  });
1026
+ const preservedParentSessionId = existingIndex?.parentSessionId
1027
+ ?? this.sessionForkRepository.findBySessionId(sessionId)?.parentSessionId
1028
+ ?? null;
1029
+ const preservedParentTitle = preservedParentSessionId
1030
+ ? this.sessionIndexRepository.findIndexRecordBySessionId(preservedParentSessionId)?.title ?? null
1031
+ : null;
1032
+ const preservedTitle = resolvePersistedSessionTitle(session.provider, session.title, existingIndex?.title ?? null, preservedParentTitle);
670
1033
  this.sessionIndexRepository.upsert({
671
1034
  sessionId,
672
1035
  workspaceId: workspace.id,
673
1036
  provider: session.provider,
674
- title: session.title,
1037
+ parentSessionId: preservedParentSessionId,
1038
+ sessionKind: existingIndex?.sessionKind ?? "default",
1039
+ annotationSourceMessageId: existingIndex?.annotationSourceMessageId ?? null,
1040
+ annotationSourceText: existingIndex?.annotationSourceText ?? null,
1041
+ isSubagent: existingIndex?.isSubagent ?? false,
1042
+ subagentLabel: existingIndex?.subagentLabel ?? null,
1043
+ title: preservedTitle,
675
1044
  messageCount: session.messageCount,
676
1045
  isArchived: resolveDiscoveredArchiveState(existingIndex?.isArchived ?? false, session.isArchived),
677
1046
  lastMessageAt: session.lastMessageAt,
@@ -699,14 +1068,34 @@ export class SessionHistoryService {
699
1068
  const relationMap = this.buildWorkspaceSessionRelationMap(sessions, discoveredSessionIds);
700
1069
  for (const persistedSession of persistedSessions) {
701
1070
  const relation = relationMap.get(persistedSession.sessionId);
1071
+ const resolvedParentSessionId = relation?.parentSessionId
1072
+ ?? persistedSession.existingIndex?.parentSessionId
1073
+ ?? this.sessionForkRepository.findBySessionId(persistedSession.sessionId)?.parentSessionId
1074
+ ?? null;
1075
+ const resolvedParentTitle = resolvedParentSessionId
1076
+ ? this.sessionIndexRepository.findIndexRecordBySessionId(resolvedParentSessionId)?.title ?? null
1077
+ : null;
702
1078
  this.sessionIndexRepository.upsert({
703
1079
  sessionId: persistedSession.sessionId,
704
1080
  workspaceId: workspace.id,
705
1081
  provider: persistedSession.session.provider,
706
- parentSessionId: relation?.parentSessionId ?? null,
707
- isSubagent: relation?.isSubagent ?? false,
708
- subagentLabel: relation?.subagentLabel ?? null,
709
- title: persistedSession.session.title,
1082
+ parentSessionId: resolvedParentSessionId,
1083
+ sessionKind: relation?.sessionKind
1084
+ ?? persistedSession.existingIndex?.sessionKind
1085
+ ?? "default",
1086
+ annotationSourceMessageId: relation?.annotationSourceMessageId
1087
+ ?? persistedSession.existingIndex?.annotationSourceMessageId
1088
+ ?? null,
1089
+ annotationSourceText: relation?.annotationSourceText
1090
+ ?? persistedSession.existingIndex?.annotationSourceText
1091
+ ?? null,
1092
+ isSubagent: relation?.isSubagent
1093
+ ?? persistedSession.existingIndex?.isSubagent
1094
+ ?? false,
1095
+ subagentLabel: relation?.subagentLabel
1096
+ ?? persistedSession.existingIndex?.subagentLabel
1097
+ ?? null,
1098
+ title: resolvePersistedSessionTitle(persistedSession.session.provider, persistedSession.session.title, persistedSession.existingIndex?.title ?? null, resolvedParentTitle),
710
1099
  messageCount: persistedSession.session.messageCount,
711
1100
  isArchived: resolveDiscoveredArchiveState(persistedSession.existingIndex?.isArchived ?? false, persistedSession.session.isArchived),
712
1101
  lastMessageAt: persistedSession.session.lastMessageAt,
@@ -723,16 +1112,16 @@ export class SessionHistoryService {
723
1112
  }
724
1113
  this.workspaceSessionRelations.set(workspaceId, this.buildWorkspaceSessionRelationMap(sessions, discoveredSessionIds));
725
1114
  const items = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
726
- const recentItems = items.slice(0, refreshStateCount);
1115
+ const refreshCandidates = buildSessionStateRefreshCandidates(items, refreshStateCount);
727
1116
  this.workspaceDiscoveryStatuses.set(workspaceId, {
728
1117
  refreshedAt: Date.now(),
729
1118
  isComplete: discovery.isComplete
730
1119
  });
731
1120
  if (refreshStateMode === "inline") {
732
- await this.refreshRecentSessionStates(recentItems, userId);
1121
+ await this.refreshRecentSessionStates(refreshCandidates, userId);
733
1122
  }
734
1123
  else {
735
- this.scheduleWorkspaceStateRefresh(workspaceId, userId, recentItems);
1124
+ this.scheduleWorkspaceStateRefresh(workspaceId, userId, refreshCandidates);
736
1125
  }
737
1126
  const nextItems = this.listWorkspaceSessions(workspaceId, userId);
738
1127
  logPerformance("workspace.discover_sessions", Date.now() - startedAt, {
@@ -742,7 +1131,8 @@ export class SessionHistoryService {
742
1131
  discoveredSessions: sessions.length,
743
1132
  returnedSessions: nextItems.length,
744
1133
  discoveryComplete: discovery.isComplete,
745
- refreshedStates: Math.min(items.length, refreshStateCount),
1134
+ providerDiagnostics: (discovery.providerDiagnostics ?? []).map((entry) => `${entry.provider}:${entry.status}:${Math.round(entry.durationMs)}ms`),
1135
+ refreshedStates: refreshCandidates.length,
746
1136
  discoverMs: discoverDurationMs,
747
1137
  persistMs: persistDurationMs,
748
1138
  refreshStateDeferred: refreshStateMode !== "inline"
@@ -788,7 +1178,8 @@ export class SessionHistoryService {
788
1178
  : this.sessionSyncService.readHistory(provider, providerSessionId, rawStoreRef, cursor, limit, direction);
789
1179
  return historyTask
790
1180
  .then((page) => {
791
- const messages = this.sessionMessageAttachmentService.enrichMessages(sessionId, page.messages);
1181
+ const messagesWithAttachments = this.sessionMessageAttachmentService.enrichMessages(sessionId, page.messages);
1182
+ const messages = this.enrichMessagesWithOrigin(sessionId, messagesWithAttachments);
792
1183
  this.persistSessionChangedFiles(sessionId, messages);
793
1184
  return {
794
1185
  ...page,
@@ -807,6 +1198,64 @@ export class SessionHistoryService {
807
1198
  throw mapSessionProviderError(error);
808
1199
  });
809
1200
  }
1201
+ enrichMessagesWithOrigin(sessionId, messages) {
1202
+ return this.resolveMessageOrigins(sessionId, messages);
1203
+ }
1204
+ resolveMessageOrigins(sessionId, messages) {
1205
+ const originRepository = this.sessionMessageOriginRepository;
1206
+ if (!originRepository || messages.length === 0) {
1207
+ return messages.map((message) => ({
1208
+ ...message,
1209
+ origin: null,
1210
+ originRef: null
1211
+ }));
1212
+ }
1213
+ const messageIds = [...new Set(messages.map((message) => message.messageId).filter(Boolean))];
1214
+ const originRows = originRepository.listBySessionAndMessageIds(sessionId, messageIds);
1215
+ const originByMessageId = new Map(originRows
1216
+ .filter((row) => row.messageId)
1217
+ .map((row) => [row.messageId, row]));
1218
+ const unresolvedRows = originRepository.listUnresolvedBySessionAndContents(sessionId, [...new Set(messages.map((message) => message.content).filter((content) => content.trim().length > 0))]);
1219
+ const unresolvedByContent = new Map();
1220
+ for (const row of unresolvedRows) {
1221
+ const current = unresolvedByContent.get(row.content) ?? [];
1222
+ current.push(row);
1223
+ unresolvedByContent.set(row.content, current);
1224
+ }
1225
+ return messages.map((message) => {
1226
+ const resolved = originByMessageId.get(message.messageId) ?? null;
1227
+ if (resolved) {
1228
+ return {
1229
+ ...message,
1230
+ origin: resolved.origin,
1231
+ originRef: resolved.originRef
1232
+ };
1233
+ }
1234
+ if (message.role !== "user") {
1235
+ return {
1236
+ ...message,
1237
+ origin: null,
1238
+ originRef: null
1239
+ };
1240
+ }
1241
+ const candidates = unresolvedByContent.get(message.content) ?? [];
1242
+ const matched = candidates.find((row) => isMessageAtOrAfter(message.timestamp, row.createdAt)) ?? null;
1243
+ if (!matched) {
1244
+ return {
1245
+ ...message,
1246
+ origin: null,
1247
+ originRef: null
1248
+ };
1249
+ }
1250
+ originRepository.resolveMessageId(sessionId, matched.clientRequestId, message.messageId, message.timestamp);
1251
+ unresolvedByContent.set(message.content, candidates.filter((candidate) => candidate.clientRequestId !== matched.clientRequestId));
1252
+ return {
1253
+ ...message,
1254
+ origin: matched.origin,
1255
+ originRef: matched.originRef
1256
+ };
1257
+ });
1258
+ }
810
1259
  buildWorkspaceSessionRelationMap(sessions, discoveredSessionIds) {
811
1260
  const relationMap = new Map();
812
1261
  for (const session of sessions) {
@@ -818,11 +1267,17 @@ export class SessionHistoryService {
818
1267
  ? discoveredSessionIds.get(buildProviderSessionKey(session.provider, session.parentProviderSessionId)) ??
819
1268
  this.sessionBindingRepository.findByProviderSession(session.provider, session.parentProviderSessionId)?.sessionId ??
820
1269
  null
821
- : null;
1270
+ : this.resolvePersistedParentSessionId(sessionId);
822
1271
  relationMap.set(sessionId, {
823
1272
  parentSessionId,
824
- isSubagent: Boolean(session.isSubagent || parentSessionId),
825
- subagentLabel: session.subagentLabel?.trim() || null
1273
+ sessionKind: this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.sessionKind ?? "default",
1274
+ annotationSourceMessageId: this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.annotationSourceMessageId ?? null,
1275
+ annotationSourceText: this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.annotationSourceText ?? null,
1276
+ isSubagent: session.isSubagent === true
1277
+ || this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.isSubagent === true,
1278
+ subagentLabel: session.subagentLabel?.trim()
1279
+ || this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.subagentLabel
1280
+ || null
826
1281
  });
827
1282
  }
828
1283
  return relationMap;
@@ -840,6 +1295,9 @@ export class SessionHistoryService {
840
1295
  return this.enrichSessionItem({
841
1296
  ...item,
842
1297
  parentSessionId: relation.parentSessionId,
1298
+ sessionKind: relation.sessionKind,
1299
+ annotationSourceMessageId: relation.annotationSourceMessageId,
1300
+ annotationSourceText: relation.annotationSourceText,
843
1301
  isSubagent: relation.isSubagent,
844
1302
  subagentLabel: relation.subagentLabel
845
1303
  });
@@ -851,24 +1309,36 @@ export class SessionHistoryService {
851
1309
  ? {
852
1310
  ...item,
853
1311
  parentSessionId: relation.parentSessionId,
1312
+ sessionKind: relation.sessionKind,
1313
+ annotationSourceMessageId: relation.annotationSourceMessageId,
1314
+ annotationSourceText: relation.annotationSourceText,
854
1315
  isSubagent: relation.isSubagent,
855
1316
  subagentLabel: relation.subagentLabel
856
1317
  }
857
1318
  : {
858
1319
  ...item,
859
1320
  parentSessionId: item.parentSessionId ?? null,
1321
+ sessionKind: item.sessionKind ?? "default",
1322
+ annotationSourceMessageId: item.annotationSourceMessageId ?? null,
1323
+ annotationSourceText: item.annotationSourceText ?? null,
860
1324
  isSubagent: item.isSubagent ?? false,
861
1325
  subagentLabel: item.subagentLabel ?? null
862
1326
  };
863
1327
  const resolution = this.sessionActivityAuthorityService.resolvePersistedSession(nextItem);
864
1328
  return applySessionActivityResolution(nextItem, resolution);
865
1329
  }
866
- async pullSessionHistory(sessionId, cursor, limit, sentMessageIds, onEnvelope, envelopeType, isClosed = () => false) {
1330
+ async pullSessionHistory(sessionId, cursor, limit, deliveredMessages, onEnvelope, envelopeType, isClosed = () => false) {
867
1331
  let currentCursor = cursor;
868
1332
  while (!isClosed()) {
869
1333
  const binding = this.getBindingOrThrow(sessionId);
870
1334
  const page = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, currentCursor, limit);
871
- await this.publishHistoryEnvelope(sessionId, binding, page, sentMessageIds, onEnvelope, envelopeType);
1335
+ await this.publishHistoryEnvelope(sessionId, binding, page, deliveredMessages, onEnvelope, envelopeType);
1336
+ if (envelopeType === "session.delta" &&
1337
+ shouldRefreshMutableHistoryTail(binding.provider, page, currentCursor, deliveredMessages)) {
1338
+ const tailPage = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, null, Math.max(limit, 20), "backward");
1339
+ deliveredMessages.lastMutableTailRefreshAtMs = Date.now();
1340
+ await this.publishHistoryEnvelope(sessionId, binding, tailPage, deliveredMessages, onEnvelope, envelopeType);
1341
+ }
872
1342
  currentCursor = page.cursor;
873
1343
  if (!page.nextCursor) {
874
1344
  return currentCursor;
@@ -876,19 +1346,21 @@ export class SessionHistoryService {
876
1346
  }
877
1347
  return currentCursor;
878
1348
  }
879
- async pullRecentSessionHistory(sessionId, limit, sentMessageIds, onEnvelope, envelopeType) {
1349
+ async pullRecentSessionHistory(sessionId, limit, deliveredMessages, onEnvelope, envelopeType) {
880
1350
  const binding = this.getBindingOrThrow(sessionId);
881
1351
  const knownTotalMessageCount = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.messageCount ?? null;
882
1352
  const page = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, null, limit, "backward", knownTotalMessageCount);
883
- await this.publishHistoryEnvelope(sessionId, binding, page, sentMessageIds, onEnvelope, envelopeType);
1353
+ await this.publishHistoryEnvelope(sessionId, binding, page, deliveredMessages, onEnvelope, envelopeType);
884
1354
  return page.cursor;
885
1355
  }
886
- async publishHistoryEnvelope(sessionId, binding, page, sentMessageIds, onEnvelope, envelopeType) {
1356
+ async publishHistoryEnvelope(sessionId, binding, page, deliveredMessages, onEnvelope, envelopeType) {
887
1357
  const messages = page.messages.filter((message) => {
888
- if (sentMessageIds.has(message.messageId)) {
1358
+ const nextSignature = buildDeliveredHistoryMessageSignature(message);
1359
+ const previousSignature = deliveredMessages.signaturesByMessageId.get(message.messageId);
1360
+ if (previousSignature === nextSignature) {
889
1361
  return false;
890
1362
  }
891
- sentMessageIds.add(message.messageId);
1363
+ rememberDeliveredHistoryMessage(deliveredMessages, message.messageId, nextSignature);
892
1364
  return true;
893
1365
  });
894
1366
  if (messages.length === 0) {
@@ -920,15 +1392,21 @@ export class SessionHistoryService {
920
1392
  return;
921
1393
  }
922
1394
  const nextTitle = (await this.sessionSyncService.readSessionTitle(binding.provider, binding.providerSessionId, binding.rawStoreRef)).trim();
923
- if (nextTitle.length === 0 || nextTitle === currentIndex.title) {
1395
+ const resolvedTitle = resolvePersistedSessionTitle(binding.provider, nextTitle, currentIndex.title);
1396
+ if (resolvedTitle.length === 0 || resolvedTitle === currentIndex.title) {
924
1397
  return;
925
1398
  }
926
1399
  this.sessionIndexRepository.upsert({
927
1400
  ...currentIndex,
928
- title: nextTitle,
1401
+ title: resolvedTitle,
929
1402
  updatedAt: nowIso()
930
1403
  });
931
1404
  }
1405
+ resolvePersistedParentSessionId(sessionId) {
1406
+ return (this.sessionForkRepository.findBySessionId(sessionId)?.parentSessionId
1407
+ ?? this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.parentSessionId
1408
+ ?? null);
1409
+ }
932
1410
  async ensureSessionChangedFilesIndexed(sessionId) {
933
1411
  if (this.sessionChangedFileService.hasIndexedSession(sessionId)) {
934
1412
  return;
@@ -980,7 +1458,53 @@ export class SessionHistoryService {
980
1458
  detail: "session 索引缺失"
981
1459
  });
982
1460
  }
983
- return item;
1461
+ const aliasTargetSessionId = this.findPendingSessionAliasTargetSessionId(item);
1462
+ if (!aliasTargetSessionId) {
1463
+ return item;
1464
+ }
1465
+ return this.sessionIndexRepository.findBySessionId(aliasTargetSessionId, userId) ?? item;
1466
+ }
1467
+ resolveCanonicalSessionId(sessionId, userId) {
1468
+ if (userId) {
1469
+ const item = this.sessionIndexRepository.findBySessionId(sessionId, userId);
1470
+ const aliasTargetSessionId = this.findPendingSessionAliasTargetSessionId(item);
1471
+ if (aliasTargetSessionId) {
1472
+ return aliasTargetSessionId;
1473
+ }
1474
+ }
1475
+ const binding = this.sessionBindingRepository.findBySessionId(sessionId);
1476
+ return this.findPendingSessionAliasTargetSessionId(binding) ?? sessionId;
1477
+ }
1478
+ isPendingSessionAlias(item) {
1479
+ return Boolean(this.findPendingSessionAliasTargetSessionId(item));
1480
+ }
1481
+ resolvePendingSessionAliasBinding(binding) {
1482
+ const aliasTargetSessionId = this.findPendingSessionAliasTargetSessionId(binding);
1483
+ if (!aliasTargetSessionId) {
1484
+ return null;
1485
+ }
1486
+ return this.sessionBindingRepository.findBySessionId(aliasTargetSessionId);
1487
+ }
1488
+ findPendingSessionAliasTargetSessionId(descriptor) {
1489
+ if (!descriptor || descriptor.provider !== "gemini") {
1490
+ return null;
1491
+ }
1492
+ const aliasTargetSessionId = extractPendingBindingTargetSessionId(descriptor.providerSessionId)
1493
+ ?? extractPendingBindingTargetSessionId(descriptor.rawStoreRef);
1494
+ if (!aliasTargetSessionId || aliasTargetSessionId === descriptor.sessionId) {
1495
+ return null;
1496
+ }
1497
+ const targetBinding = this.sessionBindingRepository.findBySessionId(aliasTargetSessionId);
1498
+ if (!targetBinding) {
1499
+ return null;
1500
+ }
1501
+ if (targetBinding.workspaceId !== descriptor.workspaceId
1502
+ || targetBinding.provider !== descriptor.provider
1503
+ || isPendingBindingValue(targetBinding.providerSessionId)
1504
+ || isPendingBindingValue(targetBinding.rawStoreRef)) {
1505
+ return null;
1506
+ }
1507
+ return aliasTargetSessionId;
984
1508
  }
985
1509
  async refreshRecentSessionStates(sessions, userId) {
986
1510
  for (let index = 0; index < sessions.length; index += 1) {
@@ -1146,6 +1670,9 @@ export class SessionHistoryService {
1146
1670
  this.db
1147
1671
  .prepare("DELETE FROM session_status_snapshots WHERE session_id = ?")
1148
1672
  .run(input.sourceSessionId);
1673
+ this.db
1674
+ .prepare("DELETE FROM session_forks WHERE session_id = ?")
1675
+ .run(input.sourceSessionId);
1149
1676
  this.db
1150
1677
  .prepare("DELETE FROM session_indices WHERE session_id = ?")
1151
1678
  .run(input.sourceSessionId);
@@ -1217,11 +1744,25 @@ export class SessionHistoryService {
1217
1744
  relationMap.delete(sourceSessionId);
1218
1745
  relationMap.set(targetSessionId, {
1219
1746
  parentSessionId: targetRelation?.parentSessionId ?? sourceRelation?.parentSessionId ?? fallbackParentSessionId,
1747
+ sessionKind: targetRelation?.sessionKind
1748
+ ?? sourceRelation?.sessionKind
1749
+ ?? targetIndex?.sessionKind
1750
+ ?? sourceIndex?.sessionKind
1751
+ ?? "default",
1752
+ annotationSourceMessageId: targetRelation?.annotationSourceMessageId
1753
+ ?? sourceRelation?.annotationSourceMessageId
1754
+ ?? targetIndex?.annotationSourceMessageId
1755
+ ?? sourceIndex?.annotationSourceMessageId
1756
+ ?? null,
1757
+ annotationSourceText: targetRelation?.annotationSourceText
1758
+ ?? sourceRelation?.annotationSourceText
1759
+ ?? targetIndex?.annotationSourceText
1760
+ ?? sourceIndex?.annotationSourceText
1761
+ ?? null,
1220
1762
  isSubagent: Boolean(targetRelation?.isSubagent
1221
1763
  || sourceRelation?.isSubagent
1222
1764
  || targetIndex?.isSubagent
1223
- || sourceIndex?.isSubagent
1224
- || fallbackParentSessionId),
1765
+ || sourceIndex?.isSubagent),
1225
1766
  subagentLabel: targetRelation?.subagentLabel
1226
1767
  ?? sourceRelation?.subagentLabel
1227
1768
  ?? targetIndex?.subagentLabel
@@ -1251,6 +1792,9 @@ export class SessionHistoryService {
1251
1792
  this.db
1252
1793
  .prepare("DELETE FROM session_status_snapshots WHERE session_id = ?")
1253
1794
  .run(sessionId);
1795
+ this.db
1796
+ .prepare("DELETE FROM session_forks WHERE session_id = ?")
1797
+ .run(sessionId);
1254
1798
  this.db
1255
1799
  .prepare("DELETE FROM session_indices WHERE session_id = ?")
1256
1800
  .run(sessionId);
@@ -1281,17 +1825,24 @@ export class SessionHistoryService {
1281
1825
  const current = this.sessionStateRepository.findBySessionAndUser(sessionId, userId);
1282
1826
  const inspection = inspectSessionActivity(binding.provider, binding.rawStoreRef);
1283
1827
  const timestamp = nowIso();
1828
+ const nowMs = Date.parse(timestamp);
1829
+ if (shouldClearStaleRuntimeWithoutInspection(current, inspection, nowMs)) {
1830
+ this.sessionActivityAuthorityService.clearSession(sessionId);
1831
+ }
1284
1832
  if (shouldPreserveRuntimeTerminalState(current, inspection)) {
1285
1833
  return current;
1286
1834
  }
1287
1835
  const resolution = this.sessionActivityAuthorityService.observe(buildInspectionActivityObservation(sessionId, inspection, timestamp));
1836
+ const resolvedLastEventAt = hasInspectionEvidence(inspection)
1837
+ ? resolution.lastObservedAt ?? inspection.lastEventAt ?? current?.lastEventAt ?? null
1838
+ : current?.lastEventAt ?? null;
1288
1839
  const nextRecord = {
1289
1840
  sessionId,
1290
1841
  userId,
1291
1842
  runningState: mapResolvedRunningStateToStored(resolution.runningState, current),
1292
1843
  activitySource: mapResolutionSourceToLegacyActivitySource(resolution.activityResolutionSource, inspection),
1293
1844
  favorite: current?.favorite ?? false,
1294
- lastEventAt: resolution.lastObservedAt ?? inspection.lastEventAt ?? current?.lastEventAt ?? null,
1845
+ lastEventAt: resolvedLastEventAt,
1295
1846
  completedAt: isTerminalResolvedRunningState(resolution.runningState)
1296
1847
  ? resolution.terminalAt ?? inspection.completedAtCandidate ?? current?.completedAt ?? null
1297
1848
  : null,
@@ -1351,11 +1902,52 @@ export class SessionHistoryService {
1351
1902
  });
1352
1903
  }
1353
1904
  }
1905
+ function isProviderCliBacked(provider) {
1906
+ return provider === "claude-code" || provider === "codex" || provider === "gemini" || provider === "kimi";
1907
+ }
1908
+ function buildProviderCliAvailabilitySnapshot(commandPaths) {
1909
+ return Object.freeze(Object.fromEntries(Object.entries(commandPaths).map(([provider, commandPath]) => [
1910
+ provider,
1911
+ isCommandAvailable(commandPath)
1912
+ ])));
1913
+ }
1914
+ function buildProviderCliUnavailableMessage(provider) {
1915
+ switch (provider) {
1916
+ case "claude-code":
1917
+ return "未检测到 Claude CLI";
1918
+ case "codex":
1919
+ return "未检测到 Codex CLI";
1920
+ case "gemini":
1921
+ return "未检测到 Gemini CLI";
1922
+ case "kimi":
1923
+ return "未检测到 Kimi CLI";
1924
+ default:
1925
+ return "未检测到对应 CLI";
1926
+ }
1927
+ }
1928
+ function createCodexForkTransportFactory(commandPath, homeDir) {
1929
+ return () => {
1930
+ const client = new CodexAppServerHelperClient(commandPath, { homeDir });
1931
+ const transport = client.createForkTransport();
1932
+ return {
1933
+ ...transport,
1934
+ close() {
1935
+ transport.close();
1936
+ client.dispose();
1937
+ }
1938
+ };
1939
+ };
1940
+ }
1354
1941
  function buildInspectionActivityObservation(sessionId, inspection, observedAt) {
1942
+ const resolvedRunningState = inspection.runningState === "failed"
1943
+ ? "failed"
1944
+ : inspection.completedAtCandidate
1945
+ ? "completed"
1946
+ : inspection.runningState;
1355
1947
  return {
1356
1948
  sessionId,
1357
1949
  runId: null,
1358
- runningState: inspection.runningState,
1950
+ runningState: resolvedRunningState,
1359
1951
  source: hasInspectionEvidence(inspection) ? "inferred_log" : "unknown",
1360
1952
  confidence: "weak",
1361
1953
  detail: inspection.errorDetail,
@@ -1412,6 +2004,20 @@ function clampLimit(limit) {
1412
2004
  }
1413
2005
  return Math.max(1, Math.min(Math.trunc(limit), 100));
1414
2006
  }
2007
+ function buildSessionStateRefreshCandidates(items, recentCount) {
2008
+ const recentItems = items.slice(0, recentCount);
2009
+ const activeResidues = items.filter((item) => isSessionStateRefreshCandidate(item));
2010
+ const deduped = new Map();
2011
+ for (const item of [...recentItems, ...activeResidues]) {
2012
+ deduped.set(item.sessionId, item);
2013
+ }
2014
+ return Array.from(deduped.values());
2015
+ }
2016
+ function isSessionStateRefreshCandidate(item) {
2017
+ return item.activityState === "running"
2018
+ || item.runningState === "starting"
2019
+ || item.runningState === "running";
2020
+ }
1415
2021
  function mapSessionStateRecordRow(row) {
1416
2022
  return {
1417
2023
  sessionId: row.session_id,
@@ -1477,6 +2083,9 @@ function mergeSessionIndexRecord(input) {
1477
2083
  workspaceId: input.workspaceId,
1478
2084
  provider: (input.target?.provider ?? input.source?.provider ?? input.provider),
1479
2085
  parentSessionId: input.target?.parentSessionId ?? input.source?.parentSessionId ?? null,
2086
+ sessionKind: input.target?.sessionKind ?? input.source?.sessionKind ?? "default",
2087
+ annotationSourceMessageId: input.target?.annotationSourceMessageId ?? input.source?.annotationSourceMessageId ?? null,
2088
+ annotationSourceText: input.target?.annotationSourceText ?? input.source?.annotationSourceText ?? null,
1480
2089
  isSubagent: Boolean(input.target?.isSubagent || input.source?.isSubagent),
1481
2090
  subagentLabel: input.target?.subagentLabel ?? input.source?.subagentLabel ?? null,
1482
2091
  title: pickPreferredSessionTitle(input.target?.title ?? null, input.source?.title ?? null),
@@ -1585,12 +2194,20 @@ function isPendingBindingValue(value) {
1585
2194
  function buildPendingBindingValue(provider, sessionId) {
1586
2195
  return `pending://${provider}/${sessionId}`;
1587
2196
  }
2197
+ function extractPendingBindingTargetSessionId(value) {
2198
+ if (!isPendingBindingValue(value)) {
2199
+ return null;
2200
+ }
2201
+ const normalizedValue = value.trim();
2202
+ const targetSessionId = normalizedValue.slice(normalizedValue.indexOf("/", "pending://".length) + 1).trim();
2203
+ return targetSessionId || null;
2204
+ }
1588
2205
  function isClaudePendingRuntimeRawStoreRef(rawStoreRef) {
1589
2206
  const normalizedRawStoreRef = rawStoreRef.replaceAll("\\", "/").toLowerCase();
1590
2207
  return normalizedRawStoreRef.includes("/.pending-");
1591
2208
  }
1592
2209
  function shouldShortCircuitClaudePendingHistory(provider, providerSessionId, rawStoreRef) {
1593
- if (provider !== "claude-code") {
2210
+ if (provider !== "claude-code" && provider !== "gemini") {
1594
2211
  return false;
1595
2212
  }
1596
2213
  return isPendingBindingValue(providerSessionId) || isPendingBindingValue(rawStoreRef);
@@ -1639,6 +2256,66 @@ function findClaudePendingDiscoveryDuplicate(session, existingSessions, claimedS
1639
2256
  });
1640
2257
  return activePendingCandidates.length === 1 ? activePendingCandidates[0] : null;
1641
2258
  }
2259
+ function findKimiRuntimeDiscoveryDuplicate(session, existingSessions, claimedSessionIds) {
2260
+ if (session.provider !== "kimi" || isPendingBindingValue(session.providerSessionId)) {
2261
+ return null;
2262
+ }
2263
+ const candidates = existingSessions.filter((item) => {
2264
+ if (claimedSessionIds.has(item.sessionId)) {
2265
+ return false;
2266
+ }
2267
+ if (item.provider !== "kimi" || !shouldRecoverKimiRuntimeBinding(item)) {
2268
+ return false;
2269
+ }
2270
+ return isCloseKimiSessionTimestamp(item.lastMessageAt ?? item.createdAt, session.lastMessageAt);
2271
+ });
2272
+ if (candidates.length === 1) {
2273
+ return candidates[0];
2274
+ }
2275
+ const comparableTitle = normalizeKimiComparableTitle(session.title);
2276
+ if (!comparableTitle) {
2277
+ return null;
2278
+ }
2279
+ const titleMatchedCandidates = candidates.filter((item) => normalizeKimiComparableTitle(item.title) === comparableTitle);
2280
+ return titleMatchedCandidates.length === 1
2281
+ ? titleMatchedCandidates[0]
2282
+ : null;
2283
+ }
2284
+ function shouldRecoverKimiRuntimeBinding(item) {
2285
+ if (isPendingBindingValue(item.providerSessionId)) {
2286
+ return true;
2287
+ }
2288
+ if (item.messageCount !== 0 || item.activitySource !== "runtime") {
2289
+ return false;
2290
+ }
2291
+ if (item.runningState === "starting") {
2292
+ return true;
2293
+ }
2294
+ if (item.lastErrorCode === "PROVIDER_READ_FAILED") {
2295
+ return true;
2296
+ }
2297
+ return (item.lastErrorDetail ?? "").includes("provider 会话不存在");
2298
+ }
2299
+ function normalizeKimiComparableTitle(title) {
2300
+ const normalized = title.trim().replace(/\s+/g, " ");
2301
+ if (!normalized) {
2302
+ return null;
2303
+ }
2304
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalized)
2305
+ ? null
2306
+ : normalized;
2307
+ }
2308
+ function isCloseKimiSessionTimestamp(left, right) {
2309
+ if (!left || !right) {
2310
+ return false;
2311
+ }
2312
+ const leftAt = Date.parse(left);
2313
+ const rightAt = Date.parse(right);
2314
+ if (!Number.isFinite(leftAt) || !Number.isFinite(rightAt)) {
2315
+ return false;
2316
+ }
2317
+ return Math.abs(leftAt - rightAt) <= 2 * 60 * 1_000;
2318
+ }
1642
2319
  function normalizeClaudeComparableTitle(title) {
1643
2320
  return title?.trim().replace(/\s+/g, " ").toLowerCase() ?? "";
1644
2321
  }
@@ -1670,6 +2347,54 @@ function isMessageAtOrAfter(timestamp, minTimestamp) {
1670
2347
  }
1671
2348
  return messageAt >= minAt;
1672
2349
  }
2350
+ function isAcceptedUserMessageTimestamp(provider, timestamp, minTimestamp) {
2351
+ if (provider === "kimi"
2352
+ && isSyntheticKimiHistoryTimestamp(timestamp)) {
2353
+ return true;
2354
+ }
2355
+ return isMessageAtOrAfter(timestamp, minTimestamp);
2356
+ }
2357
+ function isSyntheticKimiHistoryTimestamp(timestamp) {
2358
+ return timestamp.startsWith("2020-01-01T00:");
2359
+ }
2360
+ function createDeliveredHistoryMessageState() {
2361
+ return {
2362
+ signaturesByMessageId: new Map(),
2363
+ lastMutableTailRefreshAtMs: 0
2364
+ };
2365
+ }
2366
+ function shouldRefreshMutableHistoryTail(provider, page, cursor, deliveredMessages) {
2367
+ if (provider !== "kimi" || cursor === null || page.messages.length > 0) {
2368
+ return false;
2369
+ }
2370
+ return Date.now() - deliveredMessages.lastMutableTailRefreshAtMs >= MUTABLE_HISTORY_TAIL_REFRESH_INTERVAL_MS;
2371
+ }
2372
+ function buildDeliveredHistoryMessageSignature(message) {
2373
+ return hashContent(JSON.stringify({
2374
+ provider: message.provider,
2375
+ providerSessionId: message.providerSessionId,
2376
+ role: message.role,
2377
+ kind: message.kind,
2378
+ content: message.content,
2379
+ toolCall: message.toolCall,
2380
+ attachments: message.attachments ?? [],
2381
+ timestamp: message.timestamp,
2382
+ rawRef: message.rawRef
2383
+ }));
2384
+ }
2385
+ function rememberDeliveredHistoryMessage(state, messageId, signature) {
2386
+ if (state.signaturesByMessageId.has(messageId)) {
2387
+ state.signaturesByMessageId.delete(messageId);
2388
+ }
2389
+ state.signaturesByMessageId.set(messageId, signature);
2390
+ while (state.signaturesByMessageId.size > 2_048) {
2391
+ const oldestMessageId = state.signaturesByMessageId.keys().next().value;
2392
+ if (typeof oldestMessageId !== "string") {
2393
+ break;
2394
+ }
2395
+ state.signaturesByMessageId.delete(oldestMessageId);
2396
+ }
2397
+ }
1673
2398
  function delay(ms) {
1674
2399
  return new Promise((resolve) => {
1675
2400
  setTimeout(resolve, ms);
@@ -1730,6 +2455,56 @@ function isLegacyCodingNsRolloutSession(providerSessionId, rawStoreRef) {
1730
2455
  function shouldRemoveMissingSyntheticCodexSession(rawStoreRef) {
1731
2456
  return isSyntheticCodexRawStoreRef(rawStoreRef) && !existsSync(rawStoreRef);
1732
2457
  }
2458
+ function resolveSessionListTitle(provider, existingTitle, fallbackContent, parentTitle = null) {
2459
+ const normalizedExistingTitle = existingTitle?.trim() ?? "";
2460
+ const normalizedParentTitle = parentTitle?.trim() ?? "";
2461
+ const fallbackTitle = buildUserMessageTitle(fallbackContent, normalizedExistingTitle || "继续对话");
2462
+ if (normalizedExistingTitle.length > 0 &&
2463
+ !isSyntheticCodexSessionTitle(normalizedExistingTitle) &&
2464
+ (normalizedParentTitle.length === 0 ||
2465
+ normalizedExistingTitle !== normalizedParentTitle)) {
2466
+ return normalizedExistingTitle;
2467
+ }
2468
+ if (normalizedParentTitle.length > 0 && normalizedExistingTitle === normalizedParentTitle) {
2469
+ return fallbackTitle;
2470
+ }
2471
+ if (provider === "codex") {
2472
+ return fallbackTitle;
2473
+ }
2474
+ return normalizedExistingTitle || fallbackTitle;
2475
+ }
2476
+ function buildUserMessageTitle(content, fallbackTitle) {
2477
+ const title = content.trim().replace(/\s+/g, " ");
2478
+ return title.slice(0, 48) || fallbackTitle;
2479
+ }
2480
+ function resolvePersistedSessionTitle(provider, discoveredTitle, existingTitle, parentTitle = null) {
2481
+ const nextTitle = discoveredTitle.trim();
2482
+ const currentTitle = existingTitle?.trim() ?? "";
2483
+ const normalizedParentTitle = parentTitle?.trim() ?? "";
2484
+ if (!currentTitle) {
2485
+ if (provider === "codex" && isSyntheticCodexSessionTitle(nextTitle)) {
2486
+ return currentTitle;
2487
+ }
2488
+ if (normalizedParentTitle.length > 0 && nextTitle === normalizedParentTitle) {
2489
+ return currentTitle;
2490
+ }
2491
+ return nextTitle;
2492
+ }
2493
+ if (nextTitle.length === 0) {
2494
+ return currentTitle;
2495
+ }
2496
+ if (provider === "codex" && isSyntheticCodexSessionTitle(nextTitle)) {
2497
+ return currentTitle;
2498
+ }
2499
+ if (normalizedParentTitle.length > 0 && nextTitle === normalizedParentTitle && currentTitle !== normalizedParentTitle) {
2500
+ return currentTitle;
2501
+ }
2502
+ return nextTitle;
2503
+ }
2504
+ function isSyntheticCodexSessionTitle(title) {
2505
+ return (/^rollout-\d{4}-\d{2}-\d{2}t/i.test(title) ||
2506
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(title));
2507
+ }
1733
2508
  function shouldRemoveHiddenClaudeDebugSession(session) {
1734
2509
  const normalizedRawStoreRef = session.rawStoreRef.replaceAll("\\", "/");
1735
2510
  if (normalizedRawStoreRef.includes("/subagents/")) {
@@ -1738,11 +2513,34 @@ function shouldRemoveHiddenClaudeDebugSession(session) {
1738
2513
  return (/^agent-[^/]+$/i.test(session.providerSessionId) &&
1739
2514
  /\/agent-[^/]+\.jsonl$/i.test(normalizedRawStoreRef));
1740
2515
  }
2516
+ const STALE_RUNTIME_WITHOUT_INSPECTION_GRACE_MS = 120_000;
2517
+ function shouldClearStaleRuntimeWithoutInspection(current, inspection, nowMs) {
2518
+ if (!current || current.activitySource !== "runtime") {
2519
+ return false;
2520
+ }
2521
+ if (current.runningState !== "starting" && current.runningState !== "running") {
2522
+ return false;
2523
+ }
2524
+ if (inspection.lastEventAt || inspection.completedAtCandidate || inspection.errorCode) {
2525
+ return false;
2526
+ }
2527
+ if (!current.lastEventAt) {
2528
+ return true;
2529
+ }
2530
+ const lastEventAtMs = Date.parse(current.lastEventAt);
2531
+ if (!Number.isFinite(lastEventAtMs)) {
2532
+ return true;
2533
+ }
2534
+ return nowMs - lastEventAtMs > STALE_RUNTIME_WITHOUT_INSPECTION_GRACE_MS;
2535
+ }
1741
2536
  function shouldPreserveRuntimeTerminalState(current, inspection) {
1742
2537
  if (!current || current.activitySource !== "runtime") {
1743
2538
  return false;
1744
2539
  }
1745
- if (!inspection.lastEventAt || !current.lastEventAt) {
2540
+ if (!inspection.lastEventAt) {
2541
+ return !shouldClearStaleRuntimeWithoutInspection(current, inspection, Date.now());
2542
+ }
2543
+ if (!current.lastEventAt) {
1746
2544
  return true;
1747
2545
  }
1748
2546
  if (isTerminalRunningState(current.runningState)) {
@@ -1795,4 +2593,31 @@ function resolveActivityState(runningState, completedAt, lastSeenAt) {
1795
2593
  }
1796
2594
  return "idle";
1797
2595
  }
2596
+ function buildReconstructedForkPrompt(input) {
2597
+ const lines = [
2598
+ input.sourceTitle
2599
+ ? `源会话:${input.sourceTitle}`
2600
+ : "源会话:未命名会话",
2601
+ `源 provider:${input.sourceProvider}`,
2602
+ `目标 provider:${input.targetProvider}`,
2603
+ input.sourceType === "message"
2604
+ ? "分叉方式:从指定消息点重建后续上下文"
2605
+ : "分叉方式:从整条会话重建上下文",
2606
+ "",
2607
+ "下面是需要继承到新会话里的历史文本。",
2608
+ "请把这些内容当作已经发生过的上下文事实,不要逐条复述,也不要把它们当成新的用户问题重新回答。",
2609
+ "后续我会在这条新分支里继续追加新的指令。",
2610
+ ""
2611
+ ];
2612
+ if (input.messages.length === 0) {
2613
+ lines.push("当前没有可继承的历史文本。");
2614
+ return lines.join("\n");
2615
+ }
2616
+ for (const message of input.messages) {
2617
+ lines.push(message.role === "user" ? "[用户]" : "[助手]");
2618
+ lines.push(message.content.trim());
2619
+ lines.push("");
2620
+ }
2621
+ return lines.join("\n").trim();
2622
+ }
1798
2623
  //# sourceMappingURL=session-history-service.js.map