@jingyi0605/codingns 0.3.0 → 0.3.6

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 (261) hide show
  1. package/README.md +13 -0
  2. package/bin/codingns.mjs +880 -9
  3. package/dist/public/assets/{TerminalPage-Dfw1QUqW.js → TerminalPage-D00S4KM6.js} +19 -19
  4. package/dist/public/assets/index-BlOinYqR.js +122 -0
  5. package/dist/public/assets/index-Dg_7g6lA.css +1 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/config/env.d.ts +2 -0
  8. package/dist/server/config/env.js +35 -0
  9. package/dist/server/config/env.js.map +1 -1
  10. package/dist/server/config/opencode-base-url-resolver.d.ts +7 -0
  11. package/dist/server/config/opencode-base-url-resolver.js +48 -11
  12. package/dist/server/config/opencode-base-url-resolver.js.map +1 -1
  13. package/dist/server/config/opencode-system-probe-helper-client.d.ts +4 -0
  14. package/dist/server/config/opencode-system-probe-helper-client.js +29 -0
  15. package/dist/server/config/opencode-system-probe-helper-client.js.map +1 -1
  16. package/dist/server/config/opencode-system-probe-helper-process.d.ts +12 -0
  17. package/dist/server/config/opencode-system-probe-helper-process.js +34 -7
  18. package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -1
  19. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +144 -0
  20. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +242 -1
  21. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  22. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +133 -2
  23. package/dist/server/modules/assistant-capability/assistant-capability-service.js +395 -2
  24. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  25. package/dist/server/modules/auth/auth-service.d.ts +18 -1
  26. package/dist/server/modules/auth/auth-service.js +168 -7
  27. package/dist/server/modules/auth/auth-service.js.map +1 -1
  28. package/dist/server/modules/butler/butler-codex-model-policy.d.ts +1 -0
  29. package/dist/server/modules/butler/butler-codex-model-policy.js +36 -0
  30. package/dist/server/modules/butler/butler-codex-model-policy.js.map +1 -0
  31. package/dist/server/modules/butler/butler-control-session-service.d.ts +13 -1
  32. package/dist/server/modules/butler/butler-control-session-service.js +55 -231
  33. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  34. package/dist/server/modules/butler/butler-controller.d.ts +17 -0
  35. package/dist/server/modules/butler/butler-controller.js +20 -1
  36. package/dist/server/modules/butler/butler-controller.js.map +1 -1
  37. package/dist/server/modules/butler/butler-follow-up-service.js +7 -3
  38. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  39. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +36 -0
  40. package/dist/server/modules/butler/butler-inbox-analysis-service.js +375 -0
  41. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -0
  42. package/dist/server/modules/butler/butler-inbox-instruction-adapter.d.ts +23 -0
  43. package/dist/server/modules/butler/butler-inbox-instruction-adapter.js +97 -0
  44. package/dist/server/modules/butler/butler-inbox-instruction-adapter.js.map +1 -0
  45. package/dist/server/modules/butler/butler-inbox-service.d.ts +39 -2
  46. package/dist/server/modules/butler/butler-inbox-service.js +392 -2
  47. package/dist/server/modules/butler/butler-inbox-service.js.map +1 -1
  48. package/dist/server/modules/butler/butler-session-service.d.ts +8 -0
  49. package/dist/server/modules/butler/butler-session-service.js +205 -53
  50. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  51. package/dist/server/modules/butler/butler-session-summary-service.d.ts +1 -0
  52. package/dist/server/modules/butler/butler-session-summary-service.js +48 -23
  53. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -1
  54. package/dist/server/modules/butler/butler-workspace-context.d.ts +13 -0
  55. package/dist/server/modules/butler/butler-workspace-context.js +234 -0
  56. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -0
  57. package/dist/server/modules/client/client-controller.d.ts +6 -0
  58. package/dist/server/modules/client/client-controller.js +30 -8
  59. package/dist/server/modules/client/client-controller.js.map +1 -1
  60. package/dist/server/modules/client/client-service.d.ts +22 -10
  61. package/dist/server/modules/client/client-service.js +77 -100
  62. package/dist/server/modules/client/client-service.js.map +1 -1
  63. package/dist/server/modules/client/npm-global-package-service.d.ts +21 -0
  64. package/dist/server/modules/client/npm-global-package-service.js +210 -0
  65. package/dist/server/modules/client/npm-global-package-service.js.map +1 -0
  66. package/dist/server/modules/client/service-update-task-service.d.ts +15 -0
  67. package/dist/server/modules/client/service-update-task-service.js +147 -0
  68. package/dist/server/modules/client/service-update-task-service.js.map +1 -0
  69. package/dist/server/modules/client/service-update-types.d.ts +30 -0
  70. package/dist/server/modules/client/service-update-types.js +2 -0
  71. package/dist/server/modules/client/service-update-types.js.map +1 -0
  72. package/dist/server/modules/debug-target/debug-target-controller.d.ts +13 -0
  73. package/dist/server/modules/debug-target/debug-target-controller.js +77 -2
  74. package/dist/server/modules/debug-target/debug-target-controller.js.map +1 -1
  75. package/dist/server/modules/debug-target/debug-target-service.d.ts +11 -2
  76. package/dist/server/modules/debug-target/debug-target-service.js +138 -3
  77. package/dist/server/modules/debug-target/debug-target-service.js.map +1 -1
  78. package/dist/server/modules/git/git-command-helper-client.d.ts +2 -0
  79. package/dist/server/modules/git/git-command-helper-client.js +52 -3
  80. package/dist/server/modules/git/git-command-helper-client.js.map +1 -1
  81. package/dist/server/modules/git/git-command-helper-process.js +62 -9
  82. package/dist/server/modules/git/git-command-helper-process.js.map +1 -1
  83. package/dist/server/modules/git/git-command-runner.d.ts +1 -0
  84. package/dist/server/modules/git/git-command-runner.js +25 -0
  85. package/dist/server/modules/git/git-command-runner.js.map +1 -1
  86. package/dist/server/modules/git/git-controller.js +8 -7
  87. package/dist/server/modules/git/git-controller.js.map +1 -1
  88. package/dist/server/modules/git/git-read-service.d.ts +7 -7
  89. package/dist/server/modules/git/git-read-service.js +41 -24
  90. package/dist/server/modules/git/git-read-service.js.map +1 -1
  91. package/dist/server/modules/model-switch/cc-switch-adapter.d.ts +36 -0
  92. package/dist/server/modules/model-switch/cc-switch-adapter.js +321 -0
  93. package/dist/server/modules/model-switch/cc-switch-adapter.js.map +1 -0
  94. package/dist/server/modules/model-switch/model-switch-controller.d.ts +11 -0
  95. package/dist/server/modules/model-switch/model-switch-controller.js +30 -0
  96. package/dist/server/modules/model-switch/model-switch-controller.js.map +1 -0
  97. package/dist/server/modules/model-switch/model-switch-service.d.ts +16 -0
  98. package/dist/server/modules/model-switch/model-switch-service.js +29 -0
  99. package/dist/server/modules/model-switch/model-switch-service.js.map +1 -0
  100. package/dist/server/modules/preferences/profile-service.d.ts +1 -0
  101. package/dist/server/modules/preferences/profile-service.js +9 -0
  102. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  103. package/dist/server/modules/provider/codex-model-options.js +2 -3
  104. package/dist/server/modules/provider/codex-model-options.js.map +1 -1
  105. package/dist/server/modules/provider/opencode-model-options.js +2 -3
  106. package/dist/server/modules/provider/opencode-model-options.js.map +1 -1
  107. package/dist/server/modules/provider/provider-discovery-helper-client.d.ts +9 -4
  108. package/dist/server/modules/provider/provider-discovery-helper-client.js +87 -11
  109. package/dist/server/modules/provider/provider-discovery-helper-client.js.map +1 -1
  110. package/dist/server/modules/provider/provider-discovery-helper-process.js +52 -47
  111. package/dist/server/modules/provider/provider-discovery-helper-process.js.map +1 -1
  112. package/dist/server/modules/provider/provider-discovery-runtime.d.ts +4 -0
  113. package/dist/server/modules/provider/provider-discovery-runtime.js +65 -0
  114. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -0
  115. package/dist/server/modules/sessions/codex-app-server-helper-process.js +3 -0
  116. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  117. package/dist/server/modules/sessions/session-activity-authority-service.d.ts +3 -1
  118. package/dist/server/modules/sessions/session-activity-authority-service.js +3 -0
  119. package/dist/server/modules/sessions/session-activity-authority-service.js.map +1 -1
  120. package/dist/server/modules/sessions/session-activity-inspector.d.ts +1 -1
  121. package/dist/server/modules/sessions/session-activity-inspector.js +43 -16
  122. package/dist/server/modules/sessions/session-activity-inspector.js.map +1 -1
  123. package/dist/server/modules/sessions/session-controller.d.ts +3 -3
  124. package/dist/server/modules/sessions/session-controller.js +3 -3
  125. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  126. package/dist/server/modules/sessions/session-history-service.d.ts +8 -2
  127. package/dist/server/modules/sessions/session-history-service.js +473 -65
  128. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  129. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +9 -3
  130. package/dist/server/modules/sessions/session-live-runtime-service.js +114 -34
  131. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  132. package/dist/server/modules/sessions/session-message-attachment-service.d.ts +8 -8
  133. package/dist/server/modules/sessions/session-message-attachment-service.js +25 -34
  134. package/dist/server/modules/sessions/session-message-attachment-service.js.map +1 -1
  135. package/dist/server/modules/sessions/session-provider-error-mapper.js +7 -0
  136. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  137. package/dist/server/modules/skills/builtin-skill-service.d.ts +12 -0
  138. package/dist/server/modules/skills/builtin-skill-service.js +49 -0
  139. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -0
  140. package/dist/server/modules/skills/builtin-skills/codingns-assistant/SKILL.md +75 -0
  141. package/dist/server/modules/skills/builtin-skills/codingns-assistant/agents/openai.yaml +4 -0
  142. package/dist/server/modules/skills/builtin-skills/codingns-assistant/references/cli-workflow.md +130 -0
  143. package/dist/server/modules/skills/skill-manager-service.d.ts +7 -0
  144. package/dist/server/modules/skills/skill-manager-service.js +98 -0
  145. package/dist/server/modules/skills/skill-manager-service.js.map +1 -1
  146. package/dist/server/modules/tailscale/tailscale-helper-client.d.ts +1 -0
  147. package/dist/server/modules/tailscale/tailscale-helper-client.js +12 -0
  148. package/dist/server/modules/tailscale/tailscale-helper-client.js.map +1 -1
  149. package/dist/server/modules/tailscale/tailscale-manager.js +5 -1
  150. package/dist/server/modules/tailscale/tailscale-manager.js.map +1 -1
  151. package/dist/server/modules/tasks/task-helper-client.d.ts +5 -0
  152. package/dist/server/modules/tasks/task-helper-client.js +45 -0
  153. package/dist/server/modules/tasks/task-helper-client.js.map +1 -1
  154. package/dist/server/modules/tasks/task-helper-process-handlers.d.ts +10 -3
  155. package/dist/server/modules/tasks/task-helper-process-handlers.js +7 -5
  156. package/dist/server/modules/tasks/task-helper-process-handlers.js.map +1 -1
  157. package/dist/server/modules/tasks/task-helper-process.js +11 -1
  158. package/dist/server/modules/tasks/task-helper-process.js.map +1 -1
  159. package/dist/server/modules/tasks/task-lane-executors.js +5 -2
  160. package/dist/server/modules/tasks/task-lane-executors.js.map +1 -1
  161. package/dist/server/modules/tasks/task-types.d.ts +3 -0
  162. package/dist/server/modules/tasks/task-types.js +4 -1
  163. package/dist/server/modules/tasks/task-types.js.map +1 -1
  164. package/dist/server/modules/terminal/command-template-service.d.ts +2 -2
  165. package/dist/server/modules/terminal/command-template-service.js +14 -5
  166. package/dist/server/modules/terminal/command-template-service.js.map +1 -1
  167. package/dist/server/modules/terminal/runtime/terminal-log-writer-client.js +1 -1
  168. package/dist/server/modules/terminal/runtime/terminal-log-writer-client.js.map +1 -1
  169. package/dist/server/modules/terminal/runtime/terminal-log-writer-process.js +160 -11
  170. package/dist/server/modules/terminal/runtime/terminal-log-writer-process.js.map +1 -1
  171. package/dist/server/modules/terminal/template-port-runtime.d.ts +13 -2
  172. package/dist/server/modules/terminal/template-port-runtime.js +266 -44
  173. package/dist/server/modules/terminal/template-port-runtime.js.map +1 -1
  174. package/dist/server/modules/terminal/terminal-service.d.ts +4 -0
  175. package/dist/server/modules/terminal/terminal-service.js +65 -4
  176. package/dist/server/modules/terminal/terminal-service.js.map +1 -1
  177. package/dist/server/modules/workbench/workbench-service.js +3 -3
  178. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  179. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +1 -0
  180. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +118 -39
  181. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  182. package/dist/server/modules/workspace/workspace-code-composition.d.ts +1 -0
  183. package/dist/server/modules/workspace/workspace-code-composition.js +277 -2
  184. package/dist/server/modules/workspace/workspace-code-composition.js.map +1 -1
  185. package/dist/server/modules/workspace/workspace-service.js +54 -17
  186. package/dist/server/modules/workspace/workspace-service.js.map +1 -1
  187. package/dist/server/modules/worktree/worktree-cleanup-service.d.ts +1 -1
  188. package/dist/server/modules/worktree/worktree-cleanup-service.js +22 -17
  189. package/dist/server/modules/worktree/worktree-cleanup-service.js.map +1 -1
  190. package/dist/server/modules/worktree/worktree-controller.js +6 -5
  191. package/dist/server/modules/worktree/worktree-controller.js.map +1 -1
  192. package/dist/server/modules/worktree/worktree-manager.d.ts +1 -1
  193. package/dist/server/modules/worktree/worktree-manager.js +26 -19
  194. package/dist/server/modules/worktree/worktree-manager.js.map +1 -1
  195. package/dist/server/modules/worktree/worktree-merge-service.d.ts +2 -2
  196. package/dist/server/modules/worktree/worktree-merge-service.js +34 -27
  197. package/dist/server/modules/worktree/worktree-merge-service.js.map +1 -1
  198. package/dist/server/modules/worktree/worktree-sync-service.d.ts +1 -1
  199. package/dist/server/modules/worktree/worktree-sync-service.js +5 -3
  200. package/dist/server/modules/worktree/worktree-sync-service.js.map +1 -1
  201. package/dist/server/routes/assistant.js +24 -0
  202. package/dist/server/routes/assistant.js.map +1 -1
  203. package/dist/server/routes/butler.js +4 -0
  204. package/dist/server/routes/butler.js.map +1 -1
  205. package/dist/server/routes/client.js +2 -0
  206. package/dist/server/routes/client.js.map +1 -1
  207. package/dist/server/routes/sessions.js +1 -1
  208. package/dist/server/routes/sessions.js.map +1 -1
  209. package/dist/server/routes/system.d.ts +2 -1
  210. package/dist/server/routes/system.js +3 -1
  211. package/dist/server/routes/system.js.map +1 -1
  212. package/dist/server/server/create-server.d.ts +4 -0
  213. package/dist/server/server/create-server.js +67 -8
  214. package/dist/server/server/create-server.js.map +1 -1
  215. package/dist/server/shared/errors/app-error.d.ts +2 -0
  216. package/dist/server/shared/errors/app-error.js +2 -0
  217. package/dist/server/shared/errors/app-error.js.map +1 -1
  218. package/dist/server/shared/http/error-handler.d.ts +2 -1
  219. package/dist/server/shared/http/error-handler.js +3 -2
  220. package/dist/server/shared/http/error-handler.js.map +1 -1
  221. package/dist/server/shared/http/request-abort.d.ts +2 -0
  222. package/dist/server/shared/http/request-abort.js +38 -0
  223. package/dist/server/shared/http/request-abort.js.map +1 -0
  224. package/dist/server/storage/repositories/auth-login-attempt-repository.d.ts +9 -0
  225. package/dist/server/storage/repositories/auth-login-attempt-repository.js +59 -0
  226. package/dist/server/storage/repositories/auth-login-attempt-repository.js.map +1 -0
  227. package/dist/server/storage/repositories/butler-control-session-repository.d.ts +3 -0
  228. package/dist/server/storage/repositories/butler-control-session-repository.js +80 -4
  229. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -1
  230. package/dist/server/storage/repositories/butler-inbox-item-repository.js +54 -3
  231. package/dist/server/storage/repositories/butler-inbox-item-repository.js.map +1 -1
  232. package/dist/server/storage/repositories/terminal-instance-repository.js +1 -1
  233. package/dist/server/storage/repositories/terminal-instance-repository.js.map +1 -1
  234. package/dist/server/storage/repositories/user-preference-profile-repository.js +6 -3
  235. package/dist/server/storage/repositories/user-preference-profile-repository.js.map +1 -1
  236. package/dist/server/storage/sqlite/client.js +127 -0
  237. package/dist/server/storage/sqlite/client.js.map +1 -1
  238. package/dist/server/storage/sqlite/schema.sql +19 -1
  239. package/dist/server/types/domain.d.ts +37 -1
  240. package/dist/server/ws/workbench-ws-hub.d.ts +1 -0
  241. package/dist/server/ws/workbench-ws-hub.js +25 -3
  242. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  243. package/node_modules/@codingns/session-sync-core/dist/patch-builder.d.ts +23 -0
  244. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js +162 -0
  245. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js.map +1 -1
  246. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +1 -0
  247. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +89 -33
  248. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  249. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +18 -2
  250. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  251. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +3 -1
  252. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +238 -53
  253. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  254. package/node_modules/@codingns/session-sync-core/dist/runtime/provider-runtime-service.js +1 -0
  255. package/node_modules/@codingns/session-sync-core/dist/runtime/provider-runtime-service.js.map +1 -1
  256. package/node_modules/@codingns/session-sync-core/dist/runtime/types.d.ts +6 -2
  257. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +1 -1
  258. package/package.json +1 -1
  259. package/scripts/postinstall.mjs +33 -0
  260. package/dist/public/assets/index-DR2rPNi7.css +0 -1
  261. package/dist/public/assets/index-DTOruahn.js +0 -114
@@ -14,13 +14,15 @@ import { SessionForkRepository } from "../../storage/repositories/session-fork-r
14
14
  import { enrichClaudeCapabilities } from "../provider/claude-model-options.js";
15
15
  import { CodexModelOptionsService, createFallbackCodexModelOptions, enrichCodexCapabilities } from "../provider/codex-model-options.js";
16
16
  import { OpenCodeModelOptionsService, createFallbackOpenCodeModelOptions, enrichOpenCodeCapabilities } from "../provider/opencode-model-options.js";
17
- import { ProviderDiscoveryHelperClient } from "../provider/provider-discovery-helper-client.js";
17
+ import { getSharedProviderDiscoveryHelperClient } from "../provider/provider-discovery-helper-client.js";
18
+ import { discoverWorkspaceSessionsInRuntime } from "../provider/provider-discovery-runtime.js";
18
19
  import { createTaskManager } from "../tasks/task-manager.js";
19
20
  import { HOST_TASK_TYPES } from "../tasks/task-types.js";
20
21
  import { CodexAppServerHelperClient } from "./codex-app-server-helper-client.js";
21
22
  const RECONSTRUCTED_FORK_TARGET_PROVIDERS = new Set(["codex", "claude-code", "opencode"]);
22
23
  const FORK_RECONSTRUCTION_PAGE_SIZE = 200;
23
24
  const MAX_FORK_DEPTH = 4;
25
+ const SYNTHETIC_CODEX_SESSION_CLEANUP_GRACE_MS = 120_000;
24
26
  const SESSION_START_DEFERRED_PROVIDERS = new Set([
25
27
  "codex",
26
28
  "claude-code",
@@ -32,6 +34,7 @@ const MUTABLE_HISTORY_TAIL_REFRESH_INTERVAL_MS = 1_200;
32
34
  const WORKSPACE_DISCOVERY_BACKGROUND_MAX_AGE_MS = 15_000;
33
35
  const PROVIDER_CAPABILITY_CACHE_MAX_AGE_MS = 5_000;
34
36
  const WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE = 25;
37
+ const SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS = 150;
35
38
  export class SessionHistoryService {
36
39
  db;
37
40
  workspaceRepository;
@@ -52,7 +55,7 @@ export class SessionHistoryService {
52
55
  openCodeModelOptionsService;
53
56
  providerCliCommandPaths;
54
57
  providerCliAvailability;
55
- providerDiscoveryHelperClient = new ProviderDiscoveryHelperClient();
58
+ providerDiscoveryHelperClient = getSharedProviderDiscoveryHelperClient();
56
59
  providerSessionDiscoveryConfig;
57
60
  taskManager;
58
61
  workspaceDiscoveryStatuses = new Map();
@@ -148,8 +151,16 @@ export class SessionHistoryService {
148
151
  if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscovery)) {
149
152
  this.taskManager.register({
150
153
  taskType: HOST_TASK_TYPES.workspaceDiscovery,
154
+ executionLane: "host_background",
155
+ run: async ({ workspaceId, userId, refreshStateMode }, context) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode, context.signal)
156
+ });
157
+ }
158
+ if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscoveryScan)) {
159
+ this.taskManager.register({
160
+ taskType: HOST_TASK_TYPES.workspaceDiscoveryScan,
151
161
  executionLane: "helper_process",
152
- run: async ({ workspaceId, userId, refreshStateMode }) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode)
162
+ helperProcessHandler: "session.workspace_discovery",
163
+ run: async ({ config, workspacePath, knownSessions }, context) => await discoverWorkspaceSessionsInRuntime(config, workspacePath, knownSessions, context.signal)
153
164
  });
154
165
  }
155
166
  if (!this.taskManager.has(HOST_TASK_TYPES.providerCapabilityRefresh)) {
@@ -233,7 +244,10 @@ export class SessionHistoryService {
233
244
  async readSessionHistory(sessionId, cursor, limit, direction = "forward", userId) {
234
245
  const startedAt = Date.now();
235
246
  const resolvedSessionId = this.resolveCanonicalSessionId(sessionId, userId);
236
- const binding = this.getBindingOrThrow(resolvedSessionId);
247
+ let binding = this.getBindingOrThrow(resolvedSessionId);
248
+ if (userId) {
249
+ binding = await this.repairCodexDirtyBindingBeforeHistoryRead(resolvedSessionId, userId, binding);
250
+ }
237
251
  const current = this.sessionStatusSnapshotRepository.findBySessionId(resolvedSessionId);
238
252
  const safeLimit = clampLimit(limit);
239
253
  const knownTotalMessageCount = direction === "backward" && cursor === null
@@ -335,17 +349,17 @@ export class SessionHistoryService {
335
349
  await this.refreshSessionState(sessionId, userId);
336
350
  return this.enrichSessionItem(this.getSessionListItemOrThrow(sessionId, userId));
337
351
  }
338
- async syncSessionTitle(sessionId) {
352
+ async syncSessionTitle(sessionId, signal) {
339
353
  const binding = this.getBindingOrThrow(sessionId);
340
- await this.syncSessionTitleFromProvider(sessionId, binding);
354
+ await this.syncSessionTitleFromProvider(sessionId, binding, signal);
341
355
  }
342
- async syncWorkspaceSessionTitles(workspaceId, userId, concurrency = 1) {
356
+ async syncWorkspaceSessionTitles(workspaceId, userId, concurrency = 1, signal) {
343
357
  const sessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
344
358
  await runWithConcurrency(sessions, concurrency, async (session) => {
345
- await this.syncSessionTitle(session.sessionId).catch(() => {
359
+ await this.syncSessionTitle(session.sessionId, signal).catch(() => {
346
360
  return;
347
361
  });
348
- });
362
+ }, signal);
349
363
  }
350
364
  async listSessionChangedFiles(sessionId, userId) {
351
365
  this.getSession(sessionId, userId);
@@ -1090,41 +1104,54 @@ export class SessionHistoryService {
1090
1104
  providerSessionId: snapshot.providerSessionId,
1091
1105
  rawStoreRef: snapshot.rawStoreRef
1092
1106
  });
1093
- const currentBinding = this.sessionBindingRepository.findBySessionId(sessionId);
1094
- const timestamp = nowIso();
1095
- const duplicateBinding = this.findSameWorkspaceBindingDuplicate(sessionId, workspaceId, resolvedSnapshot);
1096
- this.db.transaction(() => {
1097
- if (duplicateBinding) {
1098
- // 运行时链路显式指定了当前 sessionId,就应该由当前会话接管同工作区里的重复底层会话。
1099
- // 否则后续事件重放或后台发现补录都会持续撞 UNIQUE(provider, provider_session_id)
1100
- this.mergeSessionIntoTarget({
1101
- workspaceId,
1102
- targetSessionId: sessionId,
1103
- sourceSessionId: duplicateBinding.sessionId,
1104
- provider: resolvedSnapshot.provider,
1105
- timestamp
1106
- });
1107
+ // discovery runtime 回填会并发命中这里;如果在事务外先看重复,再事务内写入,
1108
+ // 中间就会留下一个竞态窗口,最后直接撞 UNIQUE(provider, provider_session_id)
1109
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1110
+ try {
1111
+ this.db.transaction(() => {
1112
+ const currentBinding = this.sessionBindingRepository.findBySessionId(sessionId);
1113
+ const timestamp = nowIso();
1114
+ const duplicateBinding = this.findSameWorkspaceBindingDuplicate(sessionId, workspaceId, resolvedSnapshot);
1115
+ if (duplicateBinding) {
1116
+ // 运行时链路显式指定了当前 sessionId,就应该由当前会话接管同工作区里的重复底层会话。
1117
+ // 否则后续事件重放或后台发现补录都会持续撞 UNIQUE(provider, provider_session_id)。
1118
+ this.mergeSessionIntoTarget({
1119
+ workspaceId,
1120
+ targetSessionId: sessionId,
1121
+ sourceSessionId: duplicateBinding.sessionId,
1122
+ provider: resolvedSnapshot.provider,
1123
+ timestamp
1124
+ });
1125
+ }
1126
+ const currentIndex = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
1127
+ this.sessionBindingRepository.upsert({
1128
+ sessionId,
1129
+ workspaceId,
1130
+ provider: resolvedSnapshot.provider,
1131
+ providerSessionId: resolvedSnapshot.providerSessionId,
1132
+ rawStoreRef: resolvedSnapshot.rawStoreRef,
1133
+ createdAt: pickEarlierIso(currentBinding?.createdAt ?? null, duplicateBinding?.createdAt ?? null)
1134
+ ?? timestamp,
1135
+ updatedAt: timestamp
1136
+ });
1137
+ if (currentIndex) {
1138
+ this.sessionIndexRepository.upsert({
1139
+ ...currentIndex,
1140
+ updatedAt: timestamp
1141
+ });
1142
+ }
1143
+ })();
1144
+ return;
1107
1145
  }
1108
- const currentIndex = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
1109
- this.sessionBindingRepository.upsert({
1110
- sessionId,
1111
- workspaceId,
1112
- provider: resolvedSnapshot.provider,
1113
- providerSessionId: resolvedSnapshot.providerSessionId,
1114
- rawStoreRef: resolvedSnapshot.rawStoreRef,
1115
- createdAt: pickEarlierIso(currentBinding?.createdAt ?? null, duplicateBinding?.createdAt ?? null)
1116
- ?? timestamp,
1117
- updatedAt: timestamp
1118
- });
1119
- if (currentIndex) {
1120
- this.sessionIndexRepository.upsert({
1121
- ...currentIndex,
1122
- updatedAt: timestamp
1123
- });
1146
+ catch (error) {
1147
+ if (attempt === 0 && isSessionBindingProviderUniqueConflict(error)) {
1148
+ continue;
1149
+ }
1150
+ throw error;
1124
1151
  }
1125
- })();
1152
+ }
1126
1153
  }
1127
- async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline") {
1154
+ async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline", signal) {
1128
1155
  const startedAt = Date.now();
1129
1156
  const debugStartedAtMs = terminalDebugNowMs();
1130
1157
  const workspace = this.getWorkspaceOrThrow(workspaceId);
@@ -1145,13 +1172,16 @@ export class SessionHistoryService {
1145
1172
  const discoverStartedAt = Date.now();
1146
1173
  const existingWorkspaceSessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
1147
1174
  const knownSessions = this.buildKnownSessionSummaries(existingWorkspaceSessions, workspace.path);
1148
- const discovery = await this.providerDiscoveryHelperClient
1149
- .discoverWorkspaceSessions({
1150
- config: this.providerSessionDiscoveryConfig,
1151
- workspacePath: workspace.path,
1152
- knownSessions
1153
- })
1154
- .catch((error) => {
1175
+ const discoveryHandle = this.taskManager.enqueue(HOST_TASK_TYPES.workspaceDiscoveryScan, {
1176
+ key: workspaceId,
1177
+ source: "session_history.workspace_discovery.scan",
1178
+ input: {
1179
+ config: this.providerSessionDiscoveryConfig,
1180
+ workspacePath: workspace.path,
1181
+ knownSessions
1182
+ }
1183
+ });
1184
+ const discovery = await awaitTaskHandleWithSignal(discoveryHandle, signal).catch((error) => {
1155
1185
  throw mapSessionProviderError(error);
1156
1186
  });
1157
1187
  const sessions = discovery.sessions;
@@ -1162,7 +1192,9 @@ export class SessionHistoryService {
1162
1192
  const claimedPendingSessionIds = new Set();
1163
1193
  const persistPass1Transaction = this.db.transaction((batch) => {
1164
1194
  for (const session of batch) {
1165
- const exactExisting = this.sessionBindingRepository.findByProviderSession(session.provider, session.providerSessionId) ?? this.sessionBindingRepository.findByRawStoreRef(session.provider, session.rawStoreRef);
1195
+ const exactExisting = this.sessionBindingRepository.findByProviderSession(session.provider, session.providerSessionId) ?? (shouldMatchSessionBindingByRawStoreRef(session.provider)
1196
+ ? this.sessionBindingRepository.findByRawStoreRef(session.provider, session.rawStoreRef)
1197
+ : null);
1166
1198
  if (exactExisting && exactExisting.workspaceId !== workspaceId) {
1167
1199
  continue;
1168
1200
  }
@@ -1236,7 +1268,15 @@ export class SessionHistoryService {
1236
1268
  }
1237
1269
  });
1238
1270
  const persistPass1StartedAt = Date.now();
1239
- const persistPass1Stats = await runBatchedTransactions(sessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass1Transaction);
1271
+ const persistPass1Stats = await runBatchedTransactions(sessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass1Transaction, {
1272
+ scope: "workspace.discover_sessions.persist_pass1.batch",
1273
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1274
+ detail: {
1275
+ workspaceId,
1276
+ workspacePath: workspace.path,
1277
+ phase: "pass1"
1278
+ }
1279
+ });
1240
1280
  persistPass1DurationMs = Date.now() - persistPass1StartedAt;
1241
1281
  persistPass1BatchCount = persistPass1Stats.batchCount;
1242
1282
  persistPass1MaxBatchMs = persistPass1Stats.maxBatchMs;
@@ -1283,7 +1323,15 @@ export class SessionHistoryService {
1283
1323
  }
1284
1324
  });
1285
1325
  const persistPass2StartedAt = Date.now();
1286
- const persistPass2Stats = await runBatchedTransactions(persistedSessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass2Transaction);
1326
+ const persistPass2Stats = await runBatchedTransactions(persistedSessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass2Transaction, {
1327
+ scope: "workspace.discover_sessions.persist_pass2.batch",
1328
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1329
+ detail: {
1330
+ workspaceId,
1331
+ workspacePath: workspace.path,
1332
+ phase: "pass2"
1333
+ }
1334
+ });
1287
1335
  persistPass2DurationMs = Date.now() - persistPass2StartedAt;
1288
1336
  persistPass2BatchCount = persistPass2Stats.batchCount;
1289
1337
  persistPass2MaxBatchMs = persistPass2Stats.maxBatchMs;
@@ -1405,12 +1453,13 @@ export class SessionHistoryService {
1405
1453
  ? this.sessionSyncService.readRecentHistory(provider, providerSessionId, rawStoreRef, knownTotalMessageCount, limit)
1406
1454
  : this.sessionSyncService.readHistory(provider, providerSessionId, rawStoreRef, cursor, limit, direction);
1407
1455
  return historyTask
1408
- .then((page) => {
1409
- const messagesWithAttachments = this.sessionMessageAttachmentService.enrichMessages(sessionId, page.messages);
1456
+ .then(async (page) => {
1457
+ const sanitizedPage = await this.sanitizeForkHistoryPage(sessionId, page, cursor, direction);
1458
+ const messagesWithAttachments = this.sessionMessageAttachmentService.enrichMessages(sessionId, sanitizedPage.messages);
1410
1459
  const messages = this.enrichMessagesWithOrigin(sessionId, messagesWithAttachments);
1411
1460
  this.persistSessionChangedFiles(sessionId, messages);
1412
1461
  return {
1413
- ...page,
1462
+ ...sanitizedPage,
1414
1463
  messages
1415
1464
  };
1416
1465
  })
@@ -1429,6 +1478,56 @@ export class SessionHistoryService {
1429
1478
  enrichMessagesWithOrigin(sessionId, messages) {
1430
1479
  return this.resolveMessageOrigins(sessionId, messages);
1431
1480
  }
1481
+ async sanitizeForkHistoryPage(sessionId, page, cursor, direction) {
1482
+ if (direction !== "forward" || cursor !== null || page.messages.length === 0) {
1483
+ return page;
1484
+ }
1485
+ const forkRecord = this.sessionForkRepository.findBySessionId(sessionId);
1486
+ if (!forkRecord
1487
+ || forkRecord.forkSourceType !== "message"
1488
+ || !forkRecord.forkSourceMessageId) {
1489
+ return page;
1490
+ }
1491
+ const childSession = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
1492
+ const childCreatedAt = childSession?.createdAt?.trim() || null;
1493
+ if (!childCreatedAt) {
1494
+ return page;
1495
+ }
1496
+ const parentBinding = this.getBindingOrThrow(forkRecord.forkSourceSessionId);
1497
+ const inheritedMessages = await this.readForkSourceMessages(forkRecord.forkSourceSessionId, parentBinding, "message", forkRecord.forkSourceMessageId, null);
1498
+ const expectedInheritedCount = inheritedMessages.length;
1499
+ if (expectedInheritedCount <= 0) {
1500
+ return page;
1501
+ }
1502
+ const parentMessages = await this.readForkSourceMessages(forkRecord.forkSourceSessionId, parentBinding, "session", null, null);
1503
+ let leakedInheritedCount = countCommonHistoryPrefixLength(page.messages.slice(expectedInheritedCount), parentMessages.slice(expectedInheritedCount));
1504
+ if (leakedInheritedCount <= 0) {
1505
+ for (let index = expectedInheritedCount; index < page.messages.length; index += 1) {
1506
+ const message = page.messages[index];
1507
+ if (!message || message.timestamp > childCreatedAt) {
1508
+ break;
1509
+ }
1510
+ leakedInheritedCount += 1;
1511
+ }
1512
+ }
1513
+ if (forkRecord.inheritedPrefixMessageCount !== expectedInheritedCount) {
1514
+ this.sessionForkRepository.upsert({
1515
+ ...forkRecord,
1516
+ inheritedPrefixMessageCount: expectedInheritedCount
1517
+ });
1518
+ }
1519
+ if (leakedInheritedCount <= 0) {
1520
+ return page;
1521
+ }
1522
+ return {
1523
+ ...page,
1524
+ messages: [
1525
+ ...page.messages.slice(0, expectedInheritedCount),
1526
+ ...page.messages.slice(expectedInheritedCount + leakedInheritedCount)
1527
+ ],
1528
+ total: Math.max(0, page.total - leakedInheritedCount)
1529
+ };
1530
+ }
1432
1531
  resolveMessageOrigins(sessionId, messages) {
1433
1532
  const originRepository = this.sessionMessageOriginRepository;
1434
1533
  if (!originRepository || messages.length === 0) {
@@ -1611,7 +1710,7 @@ export class SessionHistoryService {
1611
1710
  messages
1612
1711
  });
1613
1712
  }
1614
- async syncSessionTitleFromProvider(sessionId, binding) {
1713
+ async syncSessionTitleFromProvider(sessionId, binding, signal) {
1615
1714
  const currentIndex = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
1616
1715
  if (!currentIndex) {
1617
1716
  return;
@@ -1624,7 +1723,7 @@ export class SessionHistoryService {
1624
1723
  provider: binding.provider,
1625
1724
  providerSessionId: binding.providerSessionId,
1626
1725
  rawStoreRef: binding.rawStoreRef
1627
- })).trim();
1726
+ }, signal)).trim();
1628
1727
  const resolvedTitle = resolvePersistedSessionTitle(binding.provider, nextTitle, currentIndex.title);
1629
1728
  if (resolvedTitle.length === 0 || resolvedTitle === currentIndex.title) {
1630
1729
  return;
@@ -1683,7 +1782,13 @@ export class SessionHistoryService {
1683
1782
  return workspace;
1684
1783
  }
1685
1784
  getSessionListItemOrThrow(sessionId, userId) {
1686
- const item = this.sessionIndexRepository.findBySessionId(sessionId, userId);
1785
+ const canonicalSessionId = this.resolveCanonicalSessionId(sessionId, userId);
1786
+ const item = this.findSessionListItem(canonicalSessionId, sessionId, userId)
1787
+ ?? this.repairMissingSessionListItem(canonicalSessionId, userId)
1788
+ ?? (canonicalSessionId === sessionId
1789
+ ? null
1790
+ : this.repairMissingSessionListItem(sessionId, userId))
1791
+ ?? this.findSessionListItem(canonicalSessionId, sessionId, userId);
1687
1792
  if (!item) {
1688
1793
  throw new AppError({
1689
1794
  statusCode: 500,
@@ -1697,6 +1802,75 @@ export class SessionHistoryService {
1697
1802
  }
1698
1803
  return this.sessionIndexRepository.findBySessionId(aliasTargetSessionId, userId) ?? item;
1699
1804
  }
1805
+ findSessionListItem(canonicalSessionId, sessionId, userId) {
1806
+ return (this.sessionIndexRepository.findBySessionId(canonicalSessionId, userId)
1807
+ ?? (canonicalSessionId === sessionId
1808
+ ? null
1809
+ : this.sessionIndexRepository.findBySessionId(sessionId, userId)));
1810
+ }
1811
+ repairMissingSessionListItem(sessionId, userId) {
1812
+ const binding = this.sessionBindingRepository.findBySessionId(sessionId);
1813
+ if (!binding) {
1814
+ return null;
1815
+ }
1816
+ const existingIndex = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
1817
+ const existingSnapshot = this.sessionStatusSnapshotRepository.findBySessionId(sessionId);
1818
+ const existingState = this.sessionStateRepository.findBySessionAndUser(sessionId, userId);
1819
+ const timestamp = nowIso();
1820
+ const fallbackLastMessageAt = existingIndex?.lastMessageAt
1821
+ ?? existingState?.lastEventAt
1822
+ ?? existingSnapshot?.lastSyncAt
1823
+ ?? null;
1824
+ const fallbackCreatedAt = pickEarlierIso(binding.createdAt, existingIndex?.createdAt ?? null)
1825
+ ?? timestamp;
1826
+ this.db.transaction(() => {
1827
+ this.sessionIndexRepository.upsert({
1828
+ sessionId,
1829
+ workspaceId: binding.workspaceId,
1830
+ provider: binding.provider,
1831
+ parentSessionId: existingIndex?.parentSessionId ?? this.sessionForkRepository.findBySessionId(sessionId)?.parentSessionId ?? null,
1832
+ sessionKind: existingIndex?.sessionKind ?? "default",
1833
+ annotationSourceMessageId: existingIndex?.annotationSourceMessageId ?? null,
1834
+ annotationSourceText: existingIndex?.annotationSourceText ?? null,
1835
+ isSubagent: existingIndex?.isSubagent ?? false,
1836
+ subagentLabel: existingIndex?.subagentLabel ?? null,
1837
+ title: existingIndex?.title?.trim()
1838
+ || buildRecoveredSessionTitle(binding.provider, binding.providerSessionId),
1839
+ messageCount: existingIndex?.messageCount ?? 0,
1840
+ isArchived: existingIndex?.isArchived ?? false,
1841
+ lastMessageAt: fallbackLastMessageAt,
1842
+ createdAt: fallbackCreatedAt,
1843
+ updatedAt: timestamp
1844
+ });
1845
+ if (!existingSnapshot) {
1846
+ this.sessionStatusSnapshotRepository.upsert({
1847
+ sessionId,
1848
+ syncStatus: "idle",
1849
+ syncCursor: null,
1850
+ lastSyncAt: fallbackLastMessageAt,
1851
+ lastErrorCode: null,
1852
+ lastErrorDetail: null,
1853
+ resumedAt: null,
1854
+ updatedAt: timestamp
1855
+ });
1856
+ }
1857
+ if (!existingState) {
1858
+ this.sessionStateRepository.upsert({
1859
+ sessionId,
1860
+ userId,
1861
+ runningState: inferRecoveredSessionRunningState(binding),
1862
+ activitySource: inferRecoveredSessionActivitySource(binding),
1863
+ favorite: false,
1864
+ lastEventAt: shouldRecoverSessionAsActive(binding) ? (binding.updatedAt || timestamp) : fallbackLastMessageAt,
1865
+ completedAt: null,
1866
+ lastSeenAt: null,
1867
+ updatedAt: timestamp
1868
+ });
1869
+ }
1870
+ })();
1871
+ console.warn(`[session-history] repaired missing session index for ${sessionId} (${binding.provider})`);
1872
+ return this.sessionIndexRepository.findBySessionId(sessionId, userId);
1873
+ }
1700
1874
  resolveCanonicalSessionId(sessionId, userId) {
1701
1875
  if (userId) {
1702
1876
  const item = this.sessionIndexRepository.findBySessionId(sessionId, userId);
@@ -1784,6 +1958,7 @@ export class SessionHistoryService {
1784
1958
  async cleanupStaleHiddenSessions(workspaceId, userId, sessions) {
1785
1959
  const discoveredProviderSessionIds = new Set(sessions.map((session) => buildProviderSessionKey(session.provider, session.providerSessionId)));
1786
1960
  const discoveredRawStoreRefs = new Set(sessions.map((session) => session.rawStoreRef));
1961
+ const nowMs = Date.now();
1787
1962
  const staleHiddenSessions = this.sessionIndexRepository
1788
1963
  .listByWorkspace(workspaceId, userId)
1789
1964
  .filter((session) => {
@@ -1795,10 +1970,13 @@ export class SessionHistoryService {
1795
1970
  }
1796
1971
  return ((session.provider === "codex" &&
1797
1972
  (isLegacyCodingNsRolloutSession(session.providerSessionId, session.rawStoreRef) ||
1798
- shouldRemoveMissingSyntheticCodexSession(session.rawStoreRef))) ||
1973
+ (shouldRemoveMissingSyntheticCodexSession(session.rawStoreRef) &&
1974
+ !this.shouldPreserveSyntheticCodexSession(session, nowMs)))) ||
1799
1975
  (session.provider === "claude-code" && shouldRemoveHiddenClaudeDebugSession(session)));
1800
1976
  });
1801
- if (staleHiddenSessions.length === 0) {
1977
+ const managedButlerSessionIds = this.listManagedButlerSessionIds(staleHiddenSessions.map((session) => session.sessionId));
1978
+ const deletableSessions = staleHiddenSessions.filter((session) => !managedButlerSessionIds.has(session.sessionId));
1979
+ if (deletableSessions.length === 0) {
1802
1980
  return;
1803
1981
  }
1804
1982
  const deleteTransaction = this.db.transaction((ids) => {
@@ -1806,13 +1984,61 @@ export class SessionHistoryService {
1806
1984
  this.deleteSessionById(sessionId);
1807
1985
  }
1808
1986
  });
1809
- await runBatchedTransactions(staleHiddenSessions.map((session) => session.sessionId), WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, deleteTransaction);
1987
+ await runBatchedTransactions(deletableSessions.map((session) => session.sessionId), WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, deleteTransaction, {
1988
+ scope: "workspace.discover_sessions.cleanup_hidden.batch",
1989
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1990
+ detail: {
1991
+ workspaceId,
1992
+ userId,
1993
+ phase: "cleanup_hidden"
1994
+ }
1995
+ });
1996
+ }
1997
+ listManagedButlerSessionIds(sessionIds) {
1998
+ if (sessionIds.length === 0) {
1999
+ return new Set();
2000
+ }
2001
+ const managedSessionIds = new Set();
2002
+ for (let index = 0; index < sessionIds.length; index += WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE) {
2003
+ const batch = sessionIds.slice(index, index + WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE);
2004
+ const placeholders = batch.map(() => "?").join(", ");
2005
+ const rows = this.db
2006
+ .prepare(`SELECT session_id
2007
+ FROM butler_sessions
2008
+ WHERE session_id IN (${placeholders})`)
2009
+ .all(...batch);
2010
+ for (const row of rows) {
2011
+ managedSessionIds.add(row.session_id);
2012
+ }
2013
+ }
2014
+ return managedSessionIds;
2015
+ }
2016
+ shouldPreserveSyntheticCodexSession(session, nowMs) {
2017
+ if (session.activitySource === "runtime"
2018
+ || session.runningState === "starting"
2019
+ || session.runningState === "running") {
2020
+ return true;
2021
+ }
2022
+ const hasActiveRuntimeState = this.listSessionStatesBySessionId(session.sessionId).some((state) => state.activitySource === "runtime"
2023
+ || state.runningState === "starting"
2024
+ || state.runningState === "running");
2025
+ if (hasActiveRuntimeState) {
2026
+ return true;
2027
+ }
2028
+ const latestTouchedAt = pickLaterIso(session.updatedAt, session.createdAt) ?? session.updatedAt;
2029
+ const latestTouchedAtMs = Date.parse(latestTouchedAt);
2030
+ if (!Number.isFinite(latestTouchedAtMs)) {
2031
+ return false;
2032
+ }
2033
+ return nowMs - latestTouchedAtMs <= SYNTHETIC_CODEX_SESSION_CLEANUP_GRACE_MS;
1810
2034
  }
1811
2035
  findSameWorkspaceBindingDuplicate(sessionId, workspaceId, snapshot) {
1812
2036
  if (isPendingBindingValue(snapshot.providerSessionId)) {
1813
2037
  return null;
1814
2038
  }
1815
- const existing = this.sessionBindingRepository.findByProviderSession(snapshot.provider, snapshot.providerSessionId) ?? this.sessionBindingRepository.findByRawStoreRef(snapshot.provider, snapshot.rawStoreRef);
2039
+ const existing = this.sessionBindingRepository.findByProviderSession(snapshot.provider, snapshot.providerSessionId) ?? (shouldMatchSessionBindingByRawStoreRef(snapshot.provider)
2040
+ ? this.sessionBindingRepository.findByRawStoreRef(snapshot.provider, snapshot.rawStoreRef)
2041
+ : null);
1816
2042
  if (!existing || existing.sessionId === sessionId) {
1817
2043
  return null;
1818
2044
  }
@@ -1825,11 +2051,22 @@ export class SessionHistoryService {
1825
2051
  if (input.targetSessionId === input.sourceSessionId) {
1826
2052
  return;
1827
2053
  }
1828
- const targetBinding = this.sessionBindingRepository.findBySessionId(input.targetSessionId);
1829
2054
  const sourceBinding = this.sessionBindingRepository.findBySessionId(input.sourceSessionId);
1830
- if (!targetBinding || !sourceBinding) {
2055
+ if (!sourceBinding) {
1831
2056
  return;
1832
2057
  }
2058
+ const targetBinding = this.sessionBindingRepository.findBySessionId(input.targetSessionId);
2059
+ if (!targetBinding) {
2060
+ this.sessionBindingRepository.upsert({
2061
+ sessionId: input.targetSessionId,
2062
+ workspaceId: input.workspaceId,
2063
+ provider: input.provider,
2064
+ providerSessionId: buildPendingBindingValue(input.provider, input.targetSessionId),
2065
+ rawStoreRef: buildPendingBindingValue(input.provider, input.targetSessionId),
2066
+ createdAt: sourceBinding.createdAt,
2067
+ updatedAt: input.timestamp
2068
+ });
2069
+ }
1833
2070
  const targetIndex = this.sessionIndexRepository.findIndexRecordBySessionId(input.targetSessionId);
1834
2071
  const sourceIndex = this.sessionIndexRepository.findIndexRecordBySessionId(input.sourceSessionId);
1835
2072
  const targetSnapshot = this.sessionStatusSnapshotRepository.findBySessionId(input.targetSessionId);
@@ -2128,6 +2365,18 @@ export class SessionHistoryService {
2128
2365
  });
2129
2366
  return nextRecord;
2130
2367
  }
2368
+ async repairCodexDirtyBindingBeforeHistoryRead(sessionId, userId, binding) {
2369
+ if (!shouldRepairCodexDirtyBinding(binding)) {
2370
+ return binding;
2371
+ }
2372
+ await this.discoverWorkspaceSessions(binding.workspaceId, userId, {
2373
+ force: true,
2374
+ refreshStateMode: "deferred"
2375
+ }).catch(() => {
2376
+ return [];
2377
+ });
2378
+ return this.getBindingOrThrow(sessionId);
2379
+ }
2131
2380
  resolveLiveActivityObservation(sessionId) {
2132
2381
  for (const resolver of this.liveActivityObservationResolvers) {
2133
2382
  const observation = resolver(sessionId);
@@ -2215,6 +2464,7 @@ function buildInspectionActivityObservation(sessionId, inspection, observedAt) {
2215
2464
  source: hasInspectionEvidence(inspection) ? "inferred_log" : "unknown",
2216
2465
  confidence: "weak",
2217
2466
  detail: inspection.errorDetail,
2467
+ interruptSource: null,
2218
2468
  errorCode: inspection.errorCode,
2219
2469
  observedAt: inspection.completedAtCandidate ?? inspection.lastEventAt ?? observedAt
2220
2470
  };
@@ -2448,6 +2698,35 @@ function normalizeSessionBindingSnapshot(sessionId, snapshot) {
2448
2698
  rawStoreRef: buildPendingBindingValue("claude-code", sessionId)
2449
2699
  };
2450
2700
  }
2701
+ function countCommonHistoryPrefixLength(left, right) {
2702
+ const maxLength = Math.min(left.length, right.length);
2703
+ let count = 0;
2704
+ for (; count < maxLength; count += 1) {
2705
+ if (!areHistoryMessagesEquivalent(left[count], right[count])) {
2706
+ break;
2707
+ }
2708
+ }
2709
+ return count;
2710
+ }
2711
+ function areHistoryMessagesEquivalent(left, right) {
2712
+ if (!left || !right) {
2713
+ return false;
2714
+ }
2715
+ if (left.messageId && right.messageId) {
2716
+ if (left.messageId === right.messageId) {
2717
+ return true;
2718
+ }
2719
+ }
2720
+ if (left.rawRef && right.rawRef) {
2721
+ if (left.rawRef === right.rawRef) {
2722
+ return true;
2723
+ }
2724
+ }
2725
+ return left.role === right.role
2726
+ && left.kind === right.kind
2727
+ && left.content === right.content
2728
+ && left.timestamp === right.timestamp;
2729
+ }
2451
2730
  function shouldSkipClaudePendingBinding(binding) {
2452
2731
  if (binding.provider !== "claude-code") {
2453
2732
  return false;
@@ -2460,9 +2739,24 @@ function shouldSkipClaudePendingBinding(binding) {
2460
2739
  function isPendingBindingValue(value) {
2461
2740
  return value.trim().toLowerCase().startsWith("pending://");
2462
2741
  }
2742
+ function isSessionBindingProviderUniqueConflict(error) {
2743
+ if (!(error instanceof Error)) {
2744
+ return false;
2745
+ }
2746
+ return error.message.includes("UNIQUE constraint failed: session_bindings.provider, session_bindings.provider_session_id");
2747
+ }
2463
2748
  function buildPendingBindingValue(provider, sessionId) {
2464
2749
  return `pending://${provider}/${sessionId}`;
2465
2750
  }
2751
+ function shouldRecoverSessionAsActive(binding) {
2752
+ return isPendingBindingValue(binding.providerSessionId) || isPendingBindingValue(binding.rawStoreRef);
2753
+ }
2754
+ function inferRecoveredSessionRunningState(binding) {
2755
+ return shouldRecoverSessionAsActive(binding) ? "starting" : "idle";
2756
+ }
2757
+ function inferRecoveredSessionActivitySource(binding) {
2758
+ return shouldRecoverSessionAsActive(binding) ? "runtime" : "none";
2759
+ }
2466
2760
  function buildAliasBindingValue(provider, targetSessionId, sourceSessionId) {
2467
2761
  return `alias://${provider}/${targetSessionId}/${sourceSessionId}`;
2468
2762
  }
@@ -2746,6 +3040,51 @@ function isLegacyCodingNsRolloutSession(providerSessionId, rawStoreRef) {
2746
3040
  function shouldRemoveMissingSyntheticCodexSession(rawStoreRef) {
2747
3041
  return isSyntheticCodexRawStoreRef(rawStoreRef) && !existsSync(rawStoreRef);
2748
3042
  }
3043
+ function shouldRepairCodexDirtyBinding(binding) {
3044
+ if (binding.provider !== "codex") {
3045
+ return false;
3046
+ }
3047
+ if (isSyntheticCodexRawStoreRef(binding.rawStoreRef)) {
3048
+ return false;
3049
+ }
3050
+ const expectedThreadId = binding.providerSessionId.trim();
3051
+ if (!expectedThreadId) {
3052
+ return false;
3053
+ }
3054
+ const boundThreadId = readCodexThreadIdFromRawStore(binding.rawStoreRef);
3055
+ if (boundThreadId) {
3056
+ return boundThreadId !== expectedThreadId;
3057
+ }
3058
+ return !existsSync(binding.rawStoreRef);
3059
+ }
3060
+ function readCodexThreadIdFromRawStore(filePath) {
3061
+ if (!existsSync(filePath)) {
3062
+ return null;
3063
+ }
3064
+ try {
3065
+ const firstLine = readFileSync(filePath, "utf8")
3066
+ .split(/\r?\n/, 1)
3067
+ .at(0)
3068
+ ?.trim();
3069
+ if (!firstLine) {
3070
+ return null;
3071
+ }
3072
+ const record = JSON.parse(firstLine);
3073
+ if (record.type !== "session_meta") {
3074
+ return null;
3075
+ }
3076
+ const threadId = typeof record.payload?.id === "string"
3077
+ ? record.payload.id.trim()
3078
+ : "";
3079
+ return threadId.length > 0 ? threadId : null;
3080
+ }
3081
+ catch {
3082
+ return null;
3083
+ }
3084
+ }
3085
+ function shouldMatchSessionBindingByRawStoreRef(provider) {
3086
+ return provider !== "codex";
3087
+ }
2749
3088
  function resolveSessionListTitle(provider, existingTitle, fallbackContent, parentTitle = null) {
2750
3089
  const normalizedExistingTitle = existingTitle?.trim() ?? "";
2751
3090
  const normalizedParentTitle = parentTitle?.trim() ?? "";
@@ -2768,6 +3107,24 @@ function buildUserMessageTitle(content, fallbackTitle) {
2768
3107
  const title = content.trim().replace(/\s+/g, " ");
2769
3108
  return title.slice(0, 48) || fallbackTitle;
2770
3109
  }
3110
+ function buildRecoveredSessionTitle(provider, providerSessionId) {
3111
+ if (isPendingBindingValue(providerSessionId)) {
3112
+ return "新会话";
3113
+ }
3114
+ const normalizedProvider = provider.trim().toLowerCase();
3115
+ const providerLabel = normalizedProvider === "claude-code"
3116
+ ? "Claude"
3117
+ : normalizedProvider === "codex"
3118
+ ? "Codex"
3119
+ : normalizedProvider === "gemini"
3120
+ ? "Gemini"
3121
+ : normalizedProvider === "kimi"
3122
+ ? "Kimi"
3123
+ : normalizedProvider === "opencode"
3124
+ ? "OpenCode"
3125
+ : provider;
3126
+ return `${providerLabel} 会话 ${providerSessionId.slice(0, 8)}`;
3127
+ }
2771
3128
  function resolvePersistedSessionTitle(provider, discoveredTitle, existingTitle, parentTitle = null) {
2772
3129
  const nextTitle = discoveredTitle.trim();
2773
3130
  const currentTitle = existingTitle?.trim() ?? "";
@@ -2914,13 +3271,14 @@ function buildReconstructedForkPrompt(input) {
2914
3271
  function buildProviderCapabilityCacheKey(provider, workspacePath) {
2915
3272
  return `${provider}::${workspacePath ?? ""}`;
2916
3273
  }
2917
- async function runWithConcurrency(items, concurrency, worker) {
3274
+ async function runWithConcurrency(items, concurrency, worker, signal) {
2918
3275
  const normalizedConcurrency = Math.max(1, Math.floor(concurrency) || 1);
2919
3276
  const queue = [...items];
2920
3277
  const runners = Array.from({
2921
3278
  length: Math.min(normalizedConcurrency, queue.length || 1)
2922
3279
  }, async () => {
2923
3280
  while (queue.length > 0) {
3281
+ throwIfAborted(signal);
2924
3282
  const current = queue.shift();
2925
3283
  if (current === undefined) {
2926
3284
  return;
@@ -2930,7 +3288,44 @@ async function runWithConcurrency(items, concurrency, worker) {
2930
3288
  });
2931
3289
  await Promise.all(runners);
2932
3290
  }
2933
- async function runBatchedTransactions(items, batchSize, transaction) {
3291
+ function throwIfAborted(signal) {
3292
+ if (signal?.aborted) {
3293
+ throw signal.reason ?? new Error("任务已取消");
3294
+ }
3295
+ }
3296
+ async function awaitTaskHandleWithSignal(handle, signal) {
3297
+ if (!signal) {
3298
+ return await handle.promise;
3299
+ }
3300
+ if (signal.aborted) {
3301
+ handle.cancel(getAbortMessage(signal.reason));
3302
+ throw signal.reason ?? new Error("任务已取消");
3303
+ }
3304
+ return await new Promise((resolve, reject) => {
3305
+ const onAbort = () => {
3306
+ handle.cancel(getAbortMessage(signal.reason));
3307
+ reject(signal.reason ?? new Error("任务已取消"));
3308
+ };
3309
+ signal.addEventListener("abort", onAbort, { once: true });
3310
+ handle.promise.then((value) => {
3311
+ signal.removeEventListener("abort", onAbort);
3312
+ resolve(value);
3313
+ }, (error) => {
3314
+ signal.removeEventListener("abort", onAbort);
3315
+ reject(error);
3316
+ });
3317
+ });
3318
+ }
3319
+ function getAbortMessage(reason) {
3320
+ if (reason instanceof Error && reason.message.trim().length > 0) {
3321
+ return reason.message;
3322
+ }
3323
+ if (typeof reason === "string" && reason.trim().length > 0) {
3324
+ return reason;
3325
+ }
3326
+ return "任务已取消";
3327
+ }
3328
+ async function runBatchedTransactions(items, batchSize, transaction, logOptions) {
2934
3329
  const normalizedBatchSize = Math.max(1, Math.floor(batchSize) || 1);
2935
3330
  let batchCount = 0;
2936
3331
  let maxBatchMs = 0;
@@ -2939,6 +3334,19 @@ async function runBatchedTransactions(items, batchSize, transaction) {
2939
3334
  const batchStartedAt = Date.now();
2940
3335
  transaction(batch);
2941
3336
  const batchDurationMs = Date.now() - batchStartedAt;
3337
+ const nextBatchIndex = batchCount + 1;
3338
+ if (logOptions) {
3339
+ logPerformance(logOptions.scope, batchDurationMs, {
3340
+ ...logOptions.detail,
3341
+ batchIndex: nextBatchIndex,
3342
+ batchSize: batch.length,
3343
+ batchStartIndex: index,
3344
+ totalItems: items.length,
3345
+ configuredBatchSize: normalizedBatchSize
3346
+ }, {
3347
+ thresholdMs: logOptions.thresholdMs ?? SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS
3348
+ });
3349
+ }
2942
3350
  batchCount += 1;
2943
3351
  maxBatchMs = Math.max(maxBatchMs, batchDurationMs);
2944
3352
  if (index + normalizedBatchSize < items.length) {