@jingyi0605/codingns 0.3.5 → 0.4.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 (255) hide show
  1. package/README.md +16 -0
  2. package/bin/codingns.mjs +1369 -10
  3. package/dist/public/assets/{TerminalPage-CgrfstRm.js → TerminalPage-6jHZV9Mh.js} +17 -17
  4. package/dist/public/assets/index-CSVhg7I8.js +123 -0
  5. package/dist/public/assets/index-Ce1VX19m.css +1 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/config/opencode-base-url-resolver.d.ts +7 -0
  8. package/dist/server/config/opencode-base-url-resolver.js +48 -11
  9. package/dist/server/config/opencode-base-url-resolver.js.map +1 -1
  10. package/dist/server/config/opencode-system-probe-helper-client.d.ts +4 -0
  11. package/dist/server/config/opencode-system-probe-helper-client.js +29 -0
  12. package/dist/server/config/opencode-system-probe-helper-client.js.map +1 -1
  13. package/dist/server/config/opencode-system-probe-helper-process.d.ts +12 -0
  14. package/dist/server/config/opencode-system-probe-helper-process.js +34 -7
  15. package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -1
  16. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +317 -0
  17. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +549 -1
  18. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  19. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +330 -2
  20. package/dist/server/modules/assistant-capability/assistant-capability-service.js +958 -3
  21. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  22. package/dist/server/modules/butler/assistant-automation-service.d.ts +110 -0
  23. package/dist/server/modules/butler/assistant-automation-service.js +786 -0
  24. package/dist/server/modules/butler/assistant-automation-service.js.map +1 -0
  25. package/dist/server/modules/butler/assistant-automation-trigger.d.ts +94 -0
  26. package/dist/server/modules/butler/assistant-automation-trigger.js +400 -0
  27. package/dist/server/modules/butler/assistant-automation-trigger.js.map +1 -0
  28. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +55 -0
  29. package/dist/server/modules/butler/assistant-sandbox-service.js +266 -0
  30. package/dist/server/modules/butler/assistant-sandbox-service.js.map +1 -0
  31. package/dist/server/modules/butler/butler-action-context-service.d.ts +4 -1
  32. package/dist/server/modules/butler/butler-action-context-service.js +8 -2
  33. package/dist/server/modules/butler/butler-action-context-service.js.map +1 -1
  34. package/dist/server/modules/butler/butler-control-session-service.d.ts +8 -1
  35. package/dist/server/modules/butler/butler-control-session-service.js +154 -40
  36. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  37. package/dist/server/modules/butler/butler-control-timer-scheduler.d.ts +32 -0
  38. package/dist/server/modules/butler/butler-control-timer-scheduler.js +93 -0
  39. package/dist/server/modules/butler/butler-control-timer-scheduler.js.map +1 -0
  40. package/dist/server/modules/butler/butler-control-timer-service.d.ts +42 -0
  41. package/dist/server/modules/butler/butler-control-timer-service.js +132 -0
  42. package/dist/server/modules/butler/butler-control-timer-service.js.map +1 -0
  43. package/dist/server/modules/butler/butler-controller.d.ts +42 -2
  44. package/dist/server/modules/butler/butler-controller.js +79 -12
  45. package/dist/server/modules/butler/butler-controller.js.map +1 -1
  46. package/dist/server/modules/butler/butler-follow-up-service.d.ts +9 -1
  47. package/dist/server/modules/butler/butler-follow-up-service.js +273 -181
  48. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  49. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +4 -1
  50. package/dist/server/modules/butler/butler-inbox-analysis-service.js +18 -4
  51. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -1
  52. package/dist/server/modules/butler/butler-inbox-instruction-adapter.js +7 -6
  53. package/dist/server/modules/butler/butler-inbox-instruction-adapter.js.map +1 -1
  54. package/dist/server/modules/butler/butler-profile-service.js +2 -5
  55. package/dist/server/modules/butler/butler-profile-service.js.map +1 -1
  56. package/dist/server/modules/butler/butler-project-service.d.ts +3 -1
  57. package/dist/server/modules/butler/butler-project-service.js +7 -1
  58. package/dist/server/modules/butler/butler-project-service.js.map +1 -1
  59. package/dist/server/modules/butler/butler-session-service.d.ts +3 -1
  60. package/dist/server/modules/butler/butler-session-service.js +12 -1
  61. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  62. package/dist/server/modules/butler/butler-session-summary-service.js +2 -1
  63. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -1
  64. package/dist/server/modules/butler/butler-workspace-context.d.ts +3 -0
  65. package/dist/server/modules/butler/butler-workspace-context.js +182 -51
  66. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  67. package/dist/server/modules/butler/patrol-execution-service.js +2 -1
  68. package/dist/server/modules/butler/patrol-execution-service.js.map +1 -1
  69. package/dist/server/modules/butler/provider-adapter-registry.d.ts +3 -0
  70. package/dist/server/modules/butler/provider-adapter-registry.js +18 -1
  71. package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -1
  72. package/dist/server/modules/butler/verification-run-service.d.ts +9 -2
  73. package/dist/server/modules/butler/verification-run-service.js +188 -34
  74. package/dist/server/modules/butler/verification-run-service.js.map +1 -1
  75. package/dist/server/modules/debug-target/debug-target-controller.d.ts +13 -0
  76. package/dist/server/modules/debug-target/debug-target-controller.js +77 -2
  77. package/dist/server/modules/debug-target/debug-target-controller.js.map +1 -1
  78. package/dist/server/modules/debug-target/debug-target-service.d.ts +17 -3
  79. package/dist/server/modules/debug-target/debug-target-service.js +696 -98
  80. package/dist/server/modules/debug-target/debug-target-service.js.map +1 -1
  81. package/dist/server/modules/git/git-command-helper-client.d.ts +3 -0
  82. package/dist/server/modules/git/git-command-helper-client.js +71 -29
  83. package/dist/server/modules/git/git-command-helper-client.js.map +1 -1
  84. package/dist/server/modules/git/git-command-helper-process.js +62 -9
  85. package/dist/server/modules/git/git-command-helper-process.js.map +1 -1
  86. package/dist/server/modules/git/git-command-runner.d.ts +1 -0
  87. package/dist/server/modules/git/git-command-runner.js +44 -1
  88. package/dist/server/modules/git/git-command-runner.js.map +1 -1
  89. package/dist/server/modules/git/git-controller.js +8 -7
  90. package/dist/server/modules/git/git-controller.js.map +1 -1
  91. package/dist/server/modules/git/git-read-service.d.ts +7 -7
  92. package/dist/server/modules/git/git-read-service.js +41 -24
  93. package/dist/server/modules/git/git-read-service.js.map +1 -1
  94. package/dist/server/modules/model-switch/cc-switch-adapter.js +6 -2
  95. package/dist/server/modules/model-switch/cc-switch-adapter.js.map +1 -1
  96. package/dist/server/modules/preferences/profile-service.d.ts +3 -1
  97. package/dist/server/modules/preferences/profile-service.js +74 -3
  98. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  99. package/dist/server/modules/provider/codex-model-options.js +2 -3
  100. package/dist/server/modules/provider/codex-model-options.js.map +1 -1
  101. package/dist/server/modules/provider/opencode-model-options.js +2 -3
  102. package/dist/server/modules/provider/opencode-model-options.js.map +1 -1
  103. package/dist/server/modules/provider/provider-discovery-helper-client.d.ts +14 -7
  104. package/dist/server/modules/provider/provider-discovery-helper-client.js +208 -46
  105. package/dist/server/modules/provider/provider-discovery-helper-client.js.map +1 -1
  106. package/dist/server/modules/provider/provider-discovery-helper-process.js +96 -47
  107. package/dist/server/modules/provider/provider-discovery-helper-process.js.map +1 -1
  108. package/dist/server/modules/provider/provider-discovery-runtime.d.ts +4 -0
  109. package/dist/server/modules/provider/provider-discovery-runtime.js +145 -0
  110. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -0
  111. package/dist/server/modules/sessions/claude-runtime-helper-client.js +23 -1
  112. package/dist/server/modules/sessions/claude-runtime-helper-client.js.map +1 -1
  113. package/dist/server/modules/sessions/session-history-service.d.ts +12 -3
  114. package/dist/server/modules/sessions/session-history-service.js +465 -67
  115. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  116. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +8 -0
  117. package/dist/server/modules/sessions/session-live-runtime-service.js +164 -34
  118. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  119. package/dist/server/modules/sessions/session-message-origin-utils.d.ts +12 -0
  120. package/dist/server/modules/sessions/session-message-origin-utils.js +45 -0
  121. package/dist/server/modules/sessions/session-message-origin-utils.js.map +1 -0
  122. package/dist/server/modules/sessions/session-permission-request-service.js +167 -0
  123. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  124. package/dist/server/modules/skills/builtin-skill-service.d.ts +12 -0
  125. package/dist/server/modules/skills/builtin-skill-service.js +49 -0
  126. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -0
  127. package/dist/server/modules/skills/builtin-skills/codingns-assistant/SKILL.md +82 -0
  128. package/dist/server/modules/skills/builtin-skills/codingns-assistant/agents/openai.yaml +4 -0
  129. package/dist/server/modules/skills/builtin-skills/codingns-assistant/references/cli-workflow.md +136 -0
  130. package/dist/server/modules/skills/skill-manager-service.d.ts +7 -0
  131. package/dist/server/modules/skills/skill-manager-service.js +98 -0
  132. package/dist/server/modules/skills/skill-manager-service.js.map +1 -1
  133. package/dist/server/modules/tailscale/tailscale-helper-client.d.ts +1 -0
  134. package/dist/server/modules/tailscale/tailscale-helper-client.js +12 -0
  135. package/dist/server/modules/tailscale/tailscale-helper-client.js.map +1 -1
  136. package/dist/server/modules/tasks/task-helper-client.d.ts +10 -2
  137. package/dist/server/modules/tasks/task-helper-client.js +152 -27
  138. package/dist/server/modules/tasks/task-helper-client.js.map +1 -1
  139. package/dist/server/modules/tasks/task-helper-process-handlers.d.ts +10 -3
  140. package/dist/server/modules/tasks/task-helper-process-handlers.js +7 -5
  141. package/dist/server/modules/tasks/task-helper-process-handlers.js.map +1 -1
  142. package/dist/server/modules/tasks/task-helper-process.js +104 -3
  143. package/dist/server/modules/tasks/task-helper-process.js.map +1 -1
  144. package/dist/server/modules/tasks/task-lane-executors.js +2 -2
  145. package/dist/server/modules/tasks/task-lane-executors.js.map +1 -1
  146. package/dist/server/modules/tasks/task-types.d.ts +4 -0
  147. package/dist/server/modules/tasks/task-types.js +5 -1
  148. package/dist/server/modules/tasks/task-types.js.map +1 -1
  149. package/dist/server/modules/terminal/command-template-service.d.ts +11 -2
  150. package/dist/server/modules/terminal/command-template-service.js +91 -9
  151. package/dist/server/modules/terminal/command-template-service.js.map +1 -1
  152. package/dist/server/modules/terminal/runtime/terminal-log-writer-client.js +1 -1
  153. package/dist/server/modules/terminal/runtime/terminal-log-writer-client.js.map +1 -1
  154. package/dist/server/modules/terminal/runtime/terminal-log-writer-process.js +160 -11
  155. package/dist/server/modules/terminal/runtime/terminal-log-writer-process.js.map +1 -1
  156. package/dist/server/modules/terminal/template-port-runtime.d.ts +1 -1
  157. package/dist/server/modules/terminal/template-port-runtime.js +87 -37
  158. package/dist/server/modules/terminal/template-port-runtime.js.map +1 -1
  159. package/dist/server/modules/terminal/terminal-controller.d.ts +3 -0
  160. package/dist/server/modules/terminal/terminal-controller.js +41 -0
  161. package/dist/server/modules/terminal/terminal-controller.js.map +1 -1
  162. package/dist/server/modules/terminal/terminal-service.d.ts +4 -0
  163. package/dist/server/modules/terminal/terminal-service.js +35 -1
  164. package/dist/server/modules/terminal/terminal-service.js.map +1 -1
  165. package/dist/server/modules/workbench/workbench-service.d.ts +3 -0
  166. package/dist/server/modules/workbench/workbench-service.js +7 -6
  167. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  168. package/dist/server/modules/workbench/workspace-file-watcher.d.ts +14 -6
  169. package/dist/server/modules/workbench/workspace-file-watcher.js +267 -57
  170. package/dist/server/modules/workbench/workspace-file-watcher.js.map +1 -1
  171. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +3 -0
  172. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +149 -41
  173. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  174. package/dist/server/modules/workspace/workspace-code-composition.d.ts +1 -0
  175. package/dist/server/modules/workspace/workspace-code-composition.js +183 -1
  176. package/dist/server/modules/workspace/workspace-code-composition.js.map +1 -1
  177. package/dist/server/modules/workspace/workspace-service.js +54 -17
  178. package/dist/server/modules/workspace/workspace-service.js.map +1 -1
  179. package/dist/server/modules/worktree/worktree-cleanup-service.d.ts +1 -1
  180. package/dist/server/modules/worktree/worktree-cleanup-service.js +22 -17
  181. package/dist/server/modules/worktree/worktree-cleanup-service.js.map +1 -1
  182. package/dist/server/modules/worktree/worktree-controller.js +6 -5
  183. package/dist/server/modules/worktree/worktree-controller.js.map +1 -1
  184. package/dist/server/modules/worktree/worktree-manager.d.ts +10 -2
  185. package/dist/server/modules/worktree/worktree-manager.js +35 -20
  186. package/dist/server/modules/worktree/worktree-manager.js.map +1 -1
  187. package/dist/server/modules/worktree/worktree-merge-service.d.ts +2 -2
  188. package/dist/server/modules/worktree/worktree-merge-service.js +34 -27
  189. package/dist/server/modules/worktree/worktree-merge-service.js.map +1 -1
  190. package/dist/server/modules/worktree/worktree-sync-service.d.ts +1 -1
  191. package/dist/server/modules/worktree/worktree-sync-service.js +5 -3
  192. package/dist/server/modules/worktree/worktree-sync-service.js.map +1 -1
  193. package/dist/server/routes/assistant.js +43 -0
  194. package/dist/server/routes/assistant.js.map +1 -1
  195. package/dist/server/routes/butler.js +5 -0
  196. package/dist/server/routes/butler.js.map +1 -1
  197. package/dist/server/server/create-server.d.ts +8 -0
  198. package/dist/server/server/create-server.js +51 -13
  199. package/dist/server/server/create-server.js.map +1 -1
  200. package/dist/server/shared/http/request-abort.d.ts +2 -0
  201. package/dist/server/shared/http/request-abort.js +38 -0
  202. package/dist/server/shared/http/request-abort.js.map +1 -0
  203. package/dist/server/storage/repositories/assistant-automation-run-repository.d.ts +12 -0
  204. package/dist/server/storage/repositories/assistant-automation-run-repository.js +139 -0
  205. package/dist/server/storage/repositories/assistant-automation-run-repository.js.map +1 -0
  206. package/dist/server/storage/repositories/assistant-automation-task-repository.d.ts +15 -0
  207. package/dist/server/storage/repositories/assistant-automation-task-repository.js +173 -0
  208. package/dist/server/storage/repositories/assistant-automation-task-repository.js.map +1 -0
  209. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +17 -0
  210. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +164 -0
  211. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +1 -0
  212. package/dist/server/storage/repositories/butler-control-session-repository.js +27 -3
  213. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -1
  214. package/dist/server/storage/repositories/butler-control-timer-repository.d.ts +15 -0
  215. package/dist/server/storage/repositories/butler-control-timer-repository.js +157 -0
  216. package/dist/server/storage/repositories/butler-control-timer-repository.js.map +1 -0
  217. package/dist/server/storage/repositories/user-preference-profile-repository.js +6 -3
  218. package/dist/server/storage/repositories/user-preference-profile-repository.js.map +1 -1
  219. package/dist/server/storage/sqlite/client.js +239 -2
  220. package/dist/server/storage/sqlite/client.js.map +1 -1
  221. package/dist/server/storage/sqlite/schema.sql +107 -1
  222. package/dist/server/types/domain.d.ts +89 -2
  223. package/dist/server/ws/workbench-ws-hub.d.ts +14 -7
  224. package/dist/server/ws/workbench-ws-hub.js +316 -158
  225. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  226. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +4 -1
  227. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +111 -3
  228. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  229. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +6 -1
  230. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +306 -31
  231. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  232. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +5 -1
  233. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +187 -26
  234. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  235. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +4 -1
  236. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +98 -1
  237. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -1
  238. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +2 -0
  239. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +71 -8
  240. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  241. package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
  242. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +4 -1
  243. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  244. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js +44 -0
  245. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js.map +1 -1
  246. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +9 -3
  247. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  248. package/node_modules/@codingns/session-sync-core/dist/runtime/types.d.ts +1 -0
  249. package/node_modules/@codingns/session-sync-core/dist/services.js +17 -8
  250. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  251. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +4 -0
  252. package/package.json +1 -1
  253. package/scripts/postinstall.mjs +33 -0
  254. package/dist/public/assets/index-Cek6u0b9.css +0 -1
  255. package/dist/public/assets/index-THHY79si.js +0 -122
@@ -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",
@@ -30,8 +32,13 @@ const SESSION_START_DEFERRED_PROVIDERS = new Set([
30
32
  ]);
31
33
  const MUTABLE_HISTORY_TAIL_REFRESH_INTERVAL_MS = 1_200;
32
34
  const WORKSPACE_DISCOVERY_BACKGROUND_MAX_AGE_MS = 15_000;
35
+ const WORKSPACE_DISCOVERY_SCAN_CONCURRENCY = 2;
33
36
  const PROVIDER_CAPABILITY_CACHE_MAX_AGE_MS = 5_000;
34
37
  const WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE = 25;
38
+ const SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS = 150;
39
+ const WORKSPACE_STATE_REFRESH_COOLDOWN_MS = 1_500;
40
+ const SQLITE_BUSY_RETRY_LIMIT = 3;
41
+ const SQLITE_BUSY_RETRY_DELAY_MS = 100;
35
42
  export class SessionHistoryService {
36
43
  db;
37
44
  workspaceRepository;
@@ -52,14 +59,15 @@ export class SessionHistoryService {
52
59
  openCodeModelOptionsService;
53
60
  providerCliCommandPaths;
54
61
  providerCliAvailability;
55
- providerDiscoveryHelperClient = new ProviderDiscoveryHelperClient();
62
+ providerDiscoveryHelperClient = getSharedProviderDiscoveryHelperClient();
56
63
  providerSessionDiscoveryConfig;
57
64
  taskManager;
58
65
  workspaceDiscoveryStatuses = new Map();
59
- workspaceStateRefreshInflight = new Map();
66
+ workspaceStateRefreshStatuses = new Map();
60
67
  providerCapabilityCache = new Map();
61
68
  liveActivityObservationResolvers = new Set();
62
69
  workspaceSessionRelations = new Map();
70
+ workspaceStateRefreshTaskSequence = 0;
63
71
  constructor(db, workspaceRepository, sessionBindingRepository, sessionChangedFileService, sessionIndexRepository, sessionMessageAttachmentService, sessionStateRepository, sessionStatusSnapshotRepository, config, sessionActivityAuthorityService = new SessionActivityAuthorityService(), sessionMessageOriginRepository = null, sessionForkRepository = null, adapterOverrides = {}, taskManager = createTaskManager()) {
64
72
  this.db = db;
65
73
  this.workspaceRepository = workspaceRepository;
@@ -148,8 +156,17 @@ export class SessionHistoryService {
148
156
  if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscovery)) {
149
157
  this.taskManager.register({
150
158
  taskType: HOST_TASK_TYPES.workspaceDiscovery,
159
+ executionLane: "host_background",
160
+ run: async ({ workspaceId, userId, refreshStateMode }, context) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode, context.signal)
161
+ });
162
+ }
163
+ if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscoveryScan)) {
164
+ this.taskManager.register({
165
+ taskType: HOST_TASK_TYPES.workspaceDiscoveryScan,
151
166
  executionLane: "helper_process",
152
- run: async ({ workspaceId, userId, refreshStateMode }) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode)
167
+ concurrency: WORKSPACE_DISCOVERY_SCAN_CONCURRENCY,
168
+ helperProcessHandler: "session.workspace_discovery",
169
+ run: async ({ config, workspacePath, knownSessions }, context) => await discoverWorkspaceSessionsInRuntime(config, workspacePath, knownSessions, context.signal)
153
170
  });
154
171
  }
155
172
  if (!this.taskManager.has(HOST_TASK_TYPES.providerCapabilityRefresh)) {
@@ -233,7 +250,10 @@ export class SessionHistoryService {
233
250
  async readSessionHistory(sessionId, cursor, limit, direction = "forward", userId) {
234
251
  const startedAt = Date.now();
235
252
  const resolvedSessionId = this.resolveCanonicalSessionId(sessionId, userId);
236
- const binding = this.getBindingOrThrow(resolvedSessionId);
253
+ let binding = this.getBindingOrThrow(resolvedSessionId);
254
+ if (userId) {
255
+ binding = await this.repairCodexDirtyBindingBeforeHistoryRead(resolvedSessionId, userId, binding);
256
+ }
237
257
  const current = this.sessionStatusSnapshotRepository.findBySessionId(resolvedSessionId);
238
258
  const safeLimit = clampLimit(limit);
239
259
  const knownTotalMessageCount = direction === "backward" && cursor === null
@@ -305,6 +325,12 @@ export class SessionHistoryService {
305
325
  originRef: null
306
326
  };
307
327
  }
328
+ resolveMessageOriginByClientRequestId(sessionId, clientRequestId, messageId, updatedAt) {
329
+ if (!this.sessionMessageOriginRepository || !clientRequestId || !messageId) {
330
+ return;
331
+ }
332
+ this.sessionMessageOriginRepository.resolveMessageId(sessionId, clientRequestId, messageId, updatedAt);
333
+ }
308
334
  async findLatestUserMessage(sessionId, content, maxAttempts = 12, minTimestamp = null) {
309
335
  const binding = this.getBindingOrThrow(sessionId);
310
336
  const acceptedContents = new Set((Array.isArray(content) ? content : [content]).filter((value) => value.trim().length > 0));
@@ -335,17 +361,17 @@ export class SessionHistoryService {
335
361
  await this.refreshSessionState(sessionId, userId);
336
362
  return this.enrichSessionItem(this.getSessionListItemOrThrow(sessionId, userId));
337
363
  }
338
- async syncSessionTitle(sessionId) {
364
+ async syncSessionTitle(sessionId, signal) {
339
365
  const binding = this.getBindingOrThrow(sessionId);
340
- await this.syncSessionTitleFromProvider(sessionId, binding);
366
+ await this.syncSessionTitleFromProvider(sessionId, binding, signal);
341
367
  }
342
- async syncWorkspaceSessionTitles(workspaceId, userId, concurrency = 1) {
368
+ async syncWorkspaceSessionTitles(workspaceId, userId, concurrency = 1, signal) {
343
369
  const sessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
344
370
  await runWithConcurrency(sessions, concurrency, async (session) => {
345
- await this.syncSessionTitle(session.sessionId).catch(() => {
371
+ await this.syncSessionTitle(session.sessionId, signal).catch(() => {
346
372
  return;
347
373
  });
348
- });
374
+ }, signal);
349
375
  }
350
376
  async listSessionChangedFiles(sessionId, userId) {
351
377
  this.getSession(sessionId, userId);
@@ -1137,7 +1163,7 @@ export class SessionHistoryService {
1137
1163
  }
1138
1164
  }
1139
1165
  }
1140
- async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline") {
1166
+ async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline", signal) {
1141
1167
  const startedAt = Date.now();
1142
1168
  const debugStartedAtMs = terminalDebugNowMs();
1143
1169
  const workspace = this.getWorkspaceOrThrow(workspaceId);
@@ -1158,13 +1184,16 @@ export class SessionHistoryService {
1158
1184
  const discoverStartedAt = Date.now();
1159
1185
  const existingWorkspaceSessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
1160
1186
  const knownSessions = this.buildKnownSessionSummaries(existingWorkspaceSessions, workspace.path);
1161
- const discovery = await this.providerDiscoveryHelperClient
1162
- .discoverWorkspaceSessions({
1163
- config: this.providerSessionDiscoveryConfig,
1164
- workspacePath: workspace.path,
1165
- knownSessions
1166
- })
1167
- .catch((error) => {
1187
+ const discoveryHandle = this.taskManager.enqueue(HOST_TASK_TYPES.workspaceDiscoveryScan, {
1188
+ key: workspaceId,
1189
+ source: "session_history.workspace_discovery.scan",
1190
+ input: {
1191
+ config: this.providerSessionDiscoveryConfig,
1192
+ workspacePath: workspace.path,
1193
+ knownSessions
1194
+ }
1195
+ });
1196
+ const discovery = await awaitTaskHandleWithSignal(discoveryHandle, signal).catch((error) => {
1168
1197
  throw mapSessionProviderError(error);
1169
1198
  });
1170
1199
  const sessions = discovery.sessions;
@@ -1198,7 +1227,7 @@ export class SessionHistoryService {
1198
1227
  const existingIndex = existing
1199
1228
  ? this.sessionIndexRepository.findIndexRecordBySessionId(existing.sessionId)
1200
1229
  : null;
1201
- this.sessionBindingRepository.upsert({
1230
+ const nextBinding = {
1202
1231
  sessionId,
1203
1232
  workspaceId: workspace.id,
1204
1233
  provider: session.provider,
@@ -1206,7 +1235,10 @@ export class SessionHistoryService {
1206
1235
  rawStoreRef: session.rawStoreRef,
1207
1236
  createdAt,
1208
1237
  updatedAt: timestamp
1209
- });
1238
+ };
1239
+ if (!areEquivalentSessionBindings(existing, nextBinding)) {
1240
+ this.sessionBindingRepository.upsert(nextBinding);
1241
+ }
1210
1242
  const preservedParentSessionId = existingIndex?.parentSessionId
1211
1243
  ?? this.sessionForkRepository.findBySessionId(sessionId)?.parentSessionId
1212
1244
  ?? null;
@@ -1214,7 +1246,7 @@ export class SessionHistoryService {
1214
1246
  ? this.sessionIndexRepository.findIndexRecordBySessionId(preservedParentSessionId)?.title ?? null
1215
1247
  : null;
1216
1248
  const preservedTitle = resolvePersistedSessionTitle(session.provider, session.title, existingIndex?.title ?? null, preservedParentTitle);
1217
- this.sessionIndexRepository.upsert({
1249
+ const nextIndex = {
1218
1250
  sessionId,
1219
1251
  workspaceId: workspace.id,
1220
1252
  provider: session.provider,
@@ -1226,12 +1258,15 @@ export class SessionHistoryService {
1226
1258
  subagentLabel: existingIndex?.subagentLabel ?? null,
1227
1259
  title: preservedTitle,
1228
1260
  messageCount: session.messageCount,
1229
- isArchived: resolveDiscoveredArchiveState(existingIndex?.isArchived ?? false, session.isArchived),
1261
+ isArchived: resolveDiscoveredArchiveState(session.provider, existingIndex?.isArchived ?? false, session.isArchived),
1230
1262
  lastMessageAt: session.lastMessageAt,
1231
1263
  createdAt,
1232
1264
  updatedAt: timestamp
1233
- });
1234
- this.sessionStatusSnapshotRepository.upsert({
1265
+ };
1266
+ if (!areEquivalentSessionIndexRecords(existingIndex, nextIndex)) {
1267
+ this.sessionIndexRepository.upsert(nextIndex);
1268
+ }
1269
+ const nextSnapshot = {
1235
1270
  sessionId,
1236
1271
  syncStatus: currentSnapshot?.syncStatus ?? "idle",
1237
1272
  syncCursor: currentSnapshot?.syncCursor ?? null,
@@ -1240,18 +1275,30 @@ export class SessionHistoryService {
1240
1275
  lastErrorDetail: currentSnapshot?.lastErrorDetail ?? null,
1241
1276
  resumedAt: currentSnapshot?.resumedAt ?? null,
1242
1277
  updatedAt: timestamp
1243
- });
1278
+ };
1279
+ if (!areEquivalentSessionStatusSnapshots(currentSnapshot, nextSnapshot)) {
1280
+ this.sessionStatusSnapshotRepository.upsert(nextSnapshot);
1281
+ }
1244
1282
  discoveredSessionIds.set(buildProviderSessionKey(session.provider, session.providerSessionId), sessionId);
1245
1283
  persistedSessions.push({
1246
1284
  session,
1247
1285
  sessionId,
1248
1286
  createdAt,
1249
- existingIndex
1287
+ existingIndex,
1288
+ pass1Index: nextIndex
1250
1289
  });
1251
1290
  }
1252
1291
  });
1253
1292
  const persistPass1StartedAt = Date.now();
1254
- const persistPass1Stats = await runBatchedTransactions(sessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass1Transaction);
1293
+ const persistPass1Stats = await runBatchedTransactions(sessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass1Transaction, {
1294
+ scope: "workspace.discover_sessions.persist_pass1.batch",
1295
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1296
+ detail: {
1297
+ workspaceId,
1298
+ workspacePath: workspace.path,
1299
+ phase: "pass1"
1300
+ }
1301
+ });
1255
1302
  persistPass1DurationMs = Date.now() - persistPass1StartedAt;
1256
1303
  persistPass1BatchCount = persistPass1Stats.batchCount;
1257
1304
  persistPass1MaxBatchMs = persistPass1Stats.maxBatchMs;
@@ -1268,7 +1315,7 @@ export class SessionHistoryService {
1268
1315
  const resolvedParentTitle = resolvedParentSessionId
1269
1316
  ? this.sessionIndexRepository.findIndexRecordBySessionId(resolvedParentSessionId)?.title ?? null
1270
1317
  : null;
1271
- this.sessionIndexRepository.upsert({
1318
+ const nextIndex = {
1272
1319
  sessionId: persistedSession.sessionId,
1273
1320
  workspaceId: workspace.id,
1274
1321
  provider: persistedSession.session.provider,
@@ -1290,15 +1337,26 @@ export class SessionHistoryService {
1290
1337
  ?? null,
1291
1338
  title: resolvePersistedSessionTitle(persistedSession.session.provider, persistedSession.session.title, persistedSession.existingIndex?.title ?? null, resolvedParentTitle),
1292
1339
  messageCount: persistedSession.session.messageCount,
1293
- isArchived: resolveDiscoveredArchiveState(persistedSession.existingIndex?.isArchived ?? false, persistedSession.session.isArchived),
1340
+ isArchived: resolveDiscoveredArchiveState(persistedSession.session.provider, persistedSession.existingIndex?.isArchived ?? false, persistedSession.session.isArchived),
1294
1341
  lastMessageAt: persistedSession.session.lastMessageAt,
1295
1342
  createdAt: persistedSession.createdAt,
1296
1343
  updatedAt: timestamp
1297
- });
1344
+ };
1345
+ if (!areEquivalentSessionIndexRecords(persistedSession.pass1Index, nextIndex)) {
1346
+ this.sessionIndexRepository.upsert(nextIndex);
1347
+ }
1298
1348
  }
1299
1349
  });
1300
1350
  const persistPass2StartedAt = Date.now();
1301
- const persistPass2Stats = await runBatchedTransactions(persistedSessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass2Transaction);
1351
+ const persistPass2Stats = await runBatchedTransactions(persistedSessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass2Transaction, {
1352
+ scope: "workspace.discover_sessions.persist_pass2.batch",
1353
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1354
+ detail: {
1355
+ workspaceId,
1356
+ workspacePath: workspace.path,
1357
+ phase: "pass2"
1358
+ }
1359
+ });
1302
1360
  persistPass2DurationMs = Date.now() - persistPass2StartedAt;
1303
1361
  persistPass2BatchCount = persistPass2Stats.batchCount;
1304
1362
  persistPass2MaxBatchMs = persistPass2Stats.maxBatchMs;
@@ -1354,7 +1412,22 @@ export class SessionHistoryService {
1354
1412
  discoveredSessions: sessions.length,
1355
1413
  returnedSessions: nextItems.length,
1356
1414
  discoveryComplete: discovery.isComplete,
1357
- providerDiagnostics: (discovery.providerDiagnostics ?? []).map((entry) => `${entry.provider}:${entry.status}:${Math.round(entry.durationMs)}ms`),
1415
+ providerDiagnostics: (discovery.providerDiagnostics ?? []).map((entry) => {
1416
+ const scannedFiles = entry.scannedFiles ?? 0;
1417
+ const skippedByMtimeSize = entry.skippedByMtimeSize ?? 0;
1418
+ const parsedFiles = entry.parsedFiles ?? 0;
1419
+ const bytesRead = entry.bytesRead ?? 0;
1420
+ return [
1421
+ entry.provider,
1422
+ entry.status,
1423
+ `${Math.round(entry.durationMs)}ms`,
1424
+ `sessions=${entry.sessionCount}`,
1425
+ `scanned=${scannedFiles}`,
1426
+ `skipped=${skippedByMtimeSize}`,
1427
+ `parsed=${parsedFiles}`,
1428
+ `bytes=${bytesRead}`
1429
+ ].join(":");
1430
+ }),
1358
1431
  refreshedStates: refreshCandidates.length,
1359
1432
  discoverMs: discoverDurationMs,
1360
1433
  persistMs: persistDurationMs,
@@ -1509,13 +1582,6 @@ export class SessionHistoryService {
1509
1582
  const originByMessageId = new Map(originRows
1510
1583
  .filter((row) => row.messageId)
1511
1584
  .map((row) => [row.messageId, row]));
1512
- const unresolvedRows = originRepository.listUnresolvedBySessionAndContents(sessionId, [...new Set(messages.map((message) => message.content).filter((content) => content.trim().length > 0))]);
1513
- const unresolvedByContent = new Map();
1514
- for (const row of unresolvedRows) {
1515
- const current = unresolvedByContent.get(row.content) ?? [];
1516
- current.push(row);
1517
- unresolvedByContent.set(row.content, current);
1518
- }
1519
1585
  return messages.map((message) => {
1520
1586
  const resolved = originByMessageId.get(message.messageId) ?? null;
1521
1587
  if (resolved) {
@@ -1532,21 +1598,10 @@ export class SessionHistoryService {
1532
1598
  originRef: null
1533
1599
  };
1534
1600
  }
1535
- const candidates = unresolvedByContent.get(message.content) ?? [];
1536
- const matched = candidates.find((row) => isMessageAtOrAfter(message.timestamp, row.createdAt)) ?? null;
1537
- if (!matched) {
1538
- return {
1539
- ...message,
1540
- origin: null,
1541
- originRef: null
1542
- };
1543
- }
1544
- originRepository.resolveMessageId(sessionId, matched.clientRequestId, message.messageId, message.timestamp);
1545
- unresolvedByContent.set(message.content, candidates.filter((candidate) => candidate.clientRequestId !== matched.clientRequestId));
1546
1601
  return {
1547
1602
  ...message,
1548
- origin: matched.origin,
1549
- originRef: matched.originRef
1603
+ origin: null,
1604
+ originRef: null
1550
1605
  };
1551
1606
  });
1552
1607
  }
@@ -1677,7 +1732,7 @@ export class SessionHistoryService {
1677
1732
  messages
1678
1733
  });
1679
1734
  }
1680
- async syncSessionTitleFromProvider(sessionId, binding) {
1735
+ async syncSessionTitleFromProvider(sessionId, binding, signal) {
1681
1736
  const currentIndex = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
1682
1737
  if (!currentIndex) {
1683
1738
  return;
@@ -1685,12 +1740,15 @@ export class SessionHistoryService {
1685
1740
  if (shouldSkipClaudePendingBinding(binding)) {
1686
1741
  return;
1687
1742
  }
1743
+ if (!shouldSyncSessionTitleFromProvider(binding.provider, currentIndex.title)) {
1744
+ return;
1745
+ }
1688
1746
  const nextTitle = (await this.providerDiscoveryHelperClient.readSessionTitle({
1689
1747
  config: this.providerSessionDiscoveryConfig,
1690
1748
  provider: binding.provider,
1691
1749
  providerSessionId: binding.providerSessionId,
1692
1750
  rawStoreRef: binding.rawStoreRef
1693
- })).trim();
1751
+ }, signal)).trim();
1694
1752
  const resolvedTitle = resolvePersistedSessionTitle(binding.provider, nextTitle, currentIndex.title);
1695
1753
  if (resolvedTitle.length === 0 || resolvedTitle === currentIndex.title) {
1696
1754
  return;
@@ -1893,13 +1951,71 @@ export class SessionHistoryService {
1893
1951
  return;
1894
1952
  }
1895
1953
  const inflightKey = `${workspaceId}:${userId}`;
1896
- if (this.workspaceStateRefreshInflight.has(inflightKey)) {
1954
+ const refreshState = this.getOrCreateWorkspaceStateRefreshStatus(inflightKey);
1955
+ const now = Date.now();
1956
+ refreshState.lastRequestedAt = now;
1957
+ refreshState.phase = refreshState.phase === "fresh" ? "stale" : refreshState.phase;
1958
+ refreshState.dirtyReasons.add("workspace.discovery.deferred_state_refresh");
1959
+ mergeWorkspaceStateRefreshSessions(refreshState.pendingSessions, sessions);
1960
+ if (refreshState.phase === "running" && refreshState.runningPromise) {
1961
+ return;
1962
+ }
1963
+ if (this.isWorkspaceStateRefreshCoolingDown(refreshState, now)) {
1964
+ refreshState.phase = "stale";
1965
+ this.ensureWorkspaceStateRefreshCooldownTimer(inflightKey, workspaceId, userId, refreshState);
1897
1966
  return;
1898
1967
  }
1968
+ this.startWorkspaceStateRefreshTask(inflightKey, workspaceId, userId, refreshState);
1969
+ }
1970
+ getOrCreateWorkspaceStateRefreshStatus(key) {
1971
+ const existing = this.workspaceStateRefreshStatuses.get(key);
1972
+ if (existing) {
1973
+ return existing;
1974
+ }
1975
+ const created = {
1976
+ phase: "fresh",
1977
+ dirtyReasons: new Set(),
1978
+ pendingSessions: new Map(),
1979
+ runningPromise: null,
1980
+ cooldownTimer: null,
1981
+ lastRequestedAt: null,
1982
+ lastStartedAt: null,
1983
+ lastCompletedAt: null,
1984
+ lastFailedAt: null,
1985
+ nextAllowedAt: null,
1986
+ runningTaskId: null
1987
+ };
1988
+ this.workspaceStateRefreshStatuses.set(key, created);
1989
+ return created;
1990
+ }
1991
+ isWorkspaceStateRefreshCoolingDown(state, now) {
1992
+ if (state.nextAllowedAt === null || now >= state.nextAllowedAt) {
1993
+ return false;
1994
+ }
1995
+ return state.phase === "cooldown" || state.phase === "failed";
1996
+ }
1997
+ startWorkspaceStateRefreshTask(key, workspaceId, userId, state) {
1998
+ if (state.runningPromise) {
1999
+ return;
2000
+ }
2001
+ const sessions = [...state.pendingSessions.values()];
2002
+ if (sessions.length === 0) {
2003
+ state.phase = "fresh";
2004
+ state.dirtyReasons.clear();
2005
+ return;
2006
+ }
2007
+ state.pendingSessions.clear();
2008
+ state.phase = "running";
2009
+ state.lastStartedAt = Date.now();
2010
+ state.runningTaskId = `${key}:${++this.workspaceStateRefreshTaskSequence}`;
1899
2011
  const startedAt = Date.now();
1900
2012
  const task = delay(0)
1901
2013
  .then(() => this.refreshRecentSessionStates(sessions, userId))
1902
2014
  .then(() => {
2015
+ state.lastCompletedAt = Date.now();
2016
+ state.phase = "cooldown";
2017
+ state.nextAllowedAt = state.lastCompletedAt + WORKSPACE_STATE_REFRESH_COOLDOWN_MS;
2018
+ state.dirtyReasons.clear();
1903
2019
  logPerformance("workspace.refresh_recent_session_states", Date.now() - startedAt, {
1904
2020
  workspaceId,
1905
2021
  refreshedStates: sessions.length
@@ -1908,6 +2024,9 @@ export class SessionHistoryService {
1908
2024
  });
1909
2025
  })
1910
2026
  .catch((error) => {
2027
+ state.lastFailedAt = Date.now();
2028
+ state.phase = "failed";
2029
+ state.nextAllowedAt = state.lastFailedAt + WORKSPACE_STATE_REFRESH_COOLDOWN_MS;
1911
2030
  logPerformance("workspace.refresh_recent_session_states.failed", Date.now() - startedAt, {
1912
2031
  workspaceId,
1913
2032
  refreshedStates: sessions.length,
@@ -1918,13 +2037,47 @@ export class SessionHistoryService {
1918
2037
  });
1919
2038
  })
1920
2039
  .finally(() => {
1921
- this.workspaceStateRefreshInflight.delete(inflightKey);
2040
+ state.runningPromise = null;
2041
+ state.runningTaskId = null;
2042
+ if (state.pendingSessions.size === 0) {
2043
+ if (state.phase === "cooldown") {
2044
+ this.ensureWorkspaceStateRefreshCooldownTimer(key, workspaceId, userId, state);
2045
+ return;
2046
+ }
2047
+ if (state.phase === "failed") {
2048
+ this.ensureWorkspaceStateRefreshCooldownTimer(key, workspaceId, userId, state);
2049
+ return;
2050
+ }
2051
+ state.phase = "fresh";
2052
+ return;
2053
+ }
2054
+ state.phase = "stale";
2055
+ this.ensureWorkspaceStateRefreshCooldownTimer(key, workspaceId, userId, state);
1922
2056
  });
1923
- this.workspaceStateRefreshInflight.set(inflightKey, task);
2057
+ state.runningPromise = task;
2058
+ }
2059
+ ensureWorkspaceStateRefreshCooldownTimer(key, workspaceId, userId, state) {
2060
+ if (state.cooldownTimer) {
2061
+ return;
2062
+ }
2063
+ const now = Date.now();
2064
+ const delayMs = Math.max(0, (state.nextAllowedAt ?? now) - now);
2065
+ state.cooldownTimer = setTimeout(() => {
2066
+ state.cooldownTimer = null;
2067
+ if (state.pendingSessions.size === 0) {
2068
+ state.phase = "fresh";
2069
+ state.dirtyReasons.clear();
2070
+ state.nextAllowedAt = null;
2071
+ return;
2072
+ }
2073
+ state.phase = "stale";
2074
+ this.startWorkspaceStateRefreshTask(key, workspaceId, userId, state);
2075
+ }, delayMs);
1924
2076
  }
1925
2077
  async cleanupStaleHiddenSessions(workspaceId, userId, sessions) {
1926
2078
  const discoveredProviderSessionIds = new Set(sessions.map((session) => buildProviderSessionKey(session.provider, session.providerSessionId)));
1927
2079
  const discoveredRawStoreRefs = new Set(sessions.map((session) => session.rawStoreRef));
2080
+ const nowMs = Date.now();
1928
2081
  const staleHiddenSessions = this.sessionIndexRepository
1929
2082
  .listByWorkspace(workspaceId, userId)
1930
2083
  .filter((session) => {
@@ -1936,10 +2089,13 @@ export class SessionHistoryService {
1936
2089
  }
1937
2090
  return ((session.provider === "codex" &&
1938
2091
  (isLegacyCodingNsRolloutSession(session.providerSessionId, session.rawStoreRef) ||
1939
- shouldRemoveMissingSyntheticCodexSession(session.rawStoreRef))) ||
2092
+ (shouldRemoveMissingSyntheticCodexSession(session.rawStoreRef) &&
2093
+ !this.shouldPreserveSyntheticCodexSession(session, nowMs)))) ||
1940
2094
  (session.provider === "claude-code" && shouldRemoveHiddenClaudeDebugSession(session)));
1941
2095
  });
1942
- if (staleHiddenSessions.length === 0) {
2096
+ const managedButlerSessionIds = this.listManagedButlerSessionIds(staleHiddenSessions.map((session) => session.sessionId));
2097
+ const deletableSessions = staleHiddenSessions.filter((session) => !managedButlerSessionIds.has(session.sessionId));
2098
+ if (deletableSessions.length === 0) {
1943
2099
  return;
1944
2100
  }
1945
2101
  const deleteTransaction = this.db.transaction((ids) => {
@@ -1947,7 +2103,53 @@ export class SessionHistoryService {
1947
2103
  this.deleteSessionById(sessionId);
1948
2104
  }
1949
2105
  });
1950
- await runBatchedTransactions(staleHiddenSessions.map((session) => session.sessionId), WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, deleteTransaction);
2106
+ await runBatchedTransactions(deletableSessions.map((session) => session.sessionId), WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, deleteTransaction, {
2107
+ scope: "workspace.discover_sessions.cleanup_hidden.batch",
2108
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
2109
+ detail: {
2110
+ workspaceId,
2111
+ userId,
2112
+ phase: "cleanup_hidden"
2113
+ }
2114
+ });
2115
+ }
2116
+ listManagedButlerSessionIds(sessionIds) {
2117
+ if (sessionIds.length === 0) {
2118
+ return new Set();
2119
+ }
2120
+ const managedSessionIds = new Set();
2121
+ for (let index = 0; index < sessionIds.length; index += WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE) {
2122
+ const batch = sessionIds.slice(index, index + WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE);
2123
+ const placeholders = batch.map(() => "?").join(", ");
2124
+ const rows = this.db
2125
+ .prepare(`SELECT session_id
2126
+ FROM butler_sessions
2127
+ WHERE session_id IN (${placeholders})`)
2128
+ .all(...batch);
2129
+ for (const row of rows) {
2130
+ managedSessionIds.add(row.session_id);
2131
+ }
2132
+ }
2133
+ return managedSessionIds;
2134
+ }
2135
+ shouldPreserveSyntheticCodexSession(session, nowMs) {
2136
+ if (session.activitySource === "runtime"
2137
+ || session.runningState === "starting"
2138
+ || session.runningState === "running") {
2139
+ return true;
2140
+ }
2141
+ const hasActiveRuntimeState = this.listSessionStatesBySessionId(session.sessionId).some((state) => state.activitySource === "runtime"
2142
+ || state.runningState === "starting"
2143
+ || state.runningState === "running");
2144
+ if (hasActiveRuntimeState) {
2145
+ return true;
2146
+ }
2147
+ const latestTouchedAt = pickLaterIso(session.updatedAt, session.createdAt) ?? session.updatedAt;
2148
+ const latestTouchedAtMs = Date.parse(latestTouchedAt);
2149
+ if (!Number.isFinite(latestTouchedAtMs)) {
2150
+ return false;
2151
+ }
2152
+ return nowMs - latestTouchedAtMs <= SYNTHETIC_CODEX_SESSION_CLEANUP_GRACE_MS;
1951
2153
  }
1952
2154
  findSameWorkspaceBindingDuplicate(sessionId, workspaceId, snapshot) {
1953
2155
  if (isPendingBindingValue(snapshot.providerSessionId)) {
@@ -2282,6 +2484,18 @@ export class SessionHistoryService {
2282
2484
  });
2283
2485
  return nextRecord;
2284
2486
  }
2487
+ async repairCodexDirtyBindingBeforeHistoryRead(sessionId, userId, binding) {
2488
+ if (!shouldRepairCodexDirtyBinding(binding)) {
2489
+ return binding;
2490
+ }
2491
+ await this.discoverWorkspaceSessions(binding.workspaceId, userId, {
2492
+ force: true,
2493
+ refreshStateMode: "deferred"
2494
+ }).catch(() => {
2495
+ return [];
2496
+ });
2497
+ return this.getBindingOrThrow(sessionId);
2498
+ }
2285
2499
  resolveLiveActivityObservation(sessionId) {
2286
2500
  for (const resolver of this.liveActivityObservationResolvers) {
2287
2501
  const observation = resolver(sessionId);
@@ -2437,6 +2651,11 @@ function buildSessionStateRefreshCandidates(items, recentCount) {
2437
2651
  }
2438
2652
  return Array.from(deduped.values());
2439
2653
  }
2654
+ function mergeWorkspaceStateRefreshSessions(target, sessions) {
2655
+ for (const session of sessions) {
2656
+ target.set(session.sessionId, session);
2657
+ }
2658
+ }
2440
2659
  function isSessionStateRefreshCandidate(item) {
2441
2660
  return item.activityState === "running"
2442
2661
  || item.runningState === "starting"
@@ -2514,12 +2733,20 @@ function mergeSessionIndexRecord(input) {
2514
2733
  subagentLabel: input.target?.subagentLabel ?? input.source?.subagentLabel ?? null,
2515
2734
  title: pickPreferredSessionTitle(input.target?.title ?? null, input.source?.title ?? null),
2516
2735
  messageCount: Math.max(input.target?.messageCount ?? 0, input.source?.messageCount ?? 0),
2517
- isArchived: Boolean(input.target?.isArchived || input.source?.isArchived),
2736
+ isArchived: mergePersistedArchiveState(input.provider, input.target?.isArchived, input.source?.isArchived),
2518
2737
  lastMessageAt: pickLaterIso(input.target?.lastMessageAt ?? null, input.source?.lastMessageAt ?? null),
2519
2738
  createdAt: pickEarlierIso(input.target?.createdAt ?? null, input.source?.createdAt ?? null) ?? input.timestamp,
2520
2739
  updatedAt: input.timestamp
2521
2740
  };
2522
2741
  }
2742
+ function mergePersistedArchiveState(provider, targetArchived, sourceArchived) {
2743
+ // 只有 Codex 这类真实支持归档的 provider 才认底层归档真相;
2744
+ // 其他 provider 的归档完全由 Host 本地索引维护,不能让旧副本把恢复状态再刷回去。
2745
+ if (shouldUseProviderDiscoveredArchiveState(provider)) {
2746
+ return Boolean(targetArchived || sourceArchived);
2747
+ }
2748
+ return targetArchived ?? sourceArchived ?? false;
2749
+ }
2523
2750
  function mergeSessionStatusSnapshot(input) {
2524
2751
  if (!input.target && !input.source) {
2525
2752
  return null;
@@ -2820,12 +3047,16 @@ function isCloseClaudeSessionTimestamp(left, right, maxGapMs = 2 * 60 * 1000) {
2820
3047
  }
2821
3048
  return Math.abs(leftAt - rightAt) <= maxGapMs;
2822
3049
  }
2823
- function resolveDiscoveredArchiveState(existingArchived, discoveredArchived) {
2824
- if (existingArchived) {
2825
- return true;
3050
+ function resolveDiscoveredArchiveState(provider, existingArchived, discoveredArchived) {
3051
+ if (!shouldUseProviderDiscoveredArchiveState(provider)) {
3052
+ return existingArchived;
2826
3053
  }
2827
3054
  return discoveredArchived === true;
2828
3055
  }
3056
+ function shouldUseProviderDiscoveredArchiveState(provider) {
3057
+ // 当前只有 Codex 的归档能稳定映射到底层文件位置;其余 provider 一律信本地 session_indices。
3058
+ return provider === "codex";
3059
+ }
2829
3060
  function isMessageAtOrAfter(timestamp, minTimestamp) {
2830
3061
  if (!minTimestamp) {
2831
3062
  return true;
@@ -2945,6 +3176,48 @@ function isLegacyCodingNsRolloutSession(providerSessionId, rawStoreRef) {
2945
3176
  function shouldRemoveMissingSyntheticCodexSession(rawStoreRef) {
2946
3177
  return isSyntheticCodexRawStoreRef(rawStoreRef) && !existsSync(rawStoreRef);
2947
3178
  }
3179
+ function shouldRepairCodexDirtyBinding(binding) {
3180
+ if (binding.provider !== "codex") {
3181
+ return false;
3182
+ }
3183
+ if (isSyntheticCodexRawStoreRef(binding.rawStoreRef)) {
3184
+ return false;
3185
+ }
3186
+ const expectedThreadId = binding.providerSessionId.trim();
3187
+ if (!expectedThreadId) {
3188
+ return false;
3189
+ }
3190
+ const boundThreadId = readCodexThreadIdFromRawStore(binding.rawStoreRef);
3191
+ if (boundThreadId) {
3192
+ return boundThreadId !== expectedThreadId;
3193
+ }
3194
+ return !existsSync(binding.rawStoreRef);
3195
+ }
3196
+ function readCodexThreadIdFromRawStore(filePath) {
3197
+ if (!existsSync(filePath)) {
3198
+ return null;
3199
+ }
3200
+ try {
3201
+ const firstLine = readFileSync(filePath, "utf8")
3202
+ .split(/\r?\n/, 1)
3203
+ .at(0)
3204
+ ?.trim();
3205
+ if (!firstLine) {
3206
+ return null;
3207
+ }
3208
+ const record = JSON.parse(firstLine);
3209
+ if (record.type !== "session_meta") {
3210
+ return null;
3211
+ }
3212
+ const threadId = typeof record.payload?.id === "string"
3213
+ ? record.payload.id.trim()
3214
+ : "";
3215
+ return threadId.length > 0 ? threadId : null;
3216
+ }
3217
+ catch {
3218
+ return null;
3219
+ }
3220
+ }
2948
3221
  function shouldMatchSessionBindingByRawStoreRef(provider) {
2949
3222
  return provider !== "codex";
2950
3223
  }
@@ -3016,6 +3289,16 @@ function isSyntheticCodexSessionTitle(title) {
3016
3289
  return (/^rollout-\d{4}-\d{2}-\d{2}t/i.test(title) ||
3017
3290
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(title));
3018
3291
  }
3292
+ function shouldSyncSessionTitleFromProvider(provider, currentTitle) {
3293
+ const normalizedTitle = currentTitle?.trim() ?? "";
3294
+ if (normalizedTitle.length === 0) {
3295
+ return true;
3296
+ }
3297
+ if (provider === "codex" && isSyntheticCodexSessionTitle(normalizedTitle)) {
3298
+ return true;
3299
+ }
3300
+ return false;
3301
+ }
3019
3302
  function shouldRemoveHiddenClaudeDebugSession(session) {
3020
3303
  const normalizedRawStoreRef = session.rawStoreRef.replaceAll("\\", "/");
3021
3304
  if (normalizedRawStoreRef.includes("/subagents/")) {
@@ -3134,13 +3417,14 @@ function buildReconstructedForkPrompt(input) {
3134
3417
  function buildProviderCapabilityCacheKey(provider, workspacePath) {
3135
3418
  return `${provider}::${workspacePath ?? ""}`;
3136
3419
  }
3137
- async function runWithConcurrency(items, concurrency, worker) {
3420
+ async function runWithConcurrency(items, concurrency, worker, signal) {
3138
3421
  const normalizedConcurrency = Math.max(1, Math.floor(concurrency) || 1);
3139
3422
  const queue = [...items];
3140
3423
  const runners = Array.from({
3141
3424
  length: Math.min(normalizedConcurrency, queue.length || 1)
3142
3425
  }, async () => {
3143
3426
  while (queue.length > 0) {
3427
+ throwIfAborted(signal);
3144
3428
  const current = queue.shift();
3145
3429
  if (current === undefined) {
3146
3430
  return;
@@ -3150,15 +3434,129 @@ async function runWithConcurrency(items, concurrency, worker) {
3150
3434
  });
3151
3435
  await Promise.all(runners);
3152
3436
  }
3153
- async function runBatchedTransactions(items, batchSize, transaction) {
3437
+ function throwIfAborted(signal) {
3438
+ if (signal?.aborted) {
3439
+ throw signal.reason ?? new Error("任务已取消");
3440
+ }
3441
+ }
3442
+ async function awaitTaskHandleWithSignal(handle, signal) {
3443
+ if (!signal) {
3444
+ return await handle.promise;
3445
+ }
3446
+ if (signal.aborted) {
3447
+ handle.cancel(getAbortMessage(signal.reason));
3448
+ throw signal.reason ?? new Error("任务已取消");
3449
+ }
3450
+ return await new Promise((resolve, reject) => {
3451
+ const onAbort = () => {
3452
+ handle.cancel(getAbortMessage(signal.reason));
3453
+ reject(signal.reason ?? new Error("任务已取消"));
3454
+ };
3455
+ signal.addEventListener("abort", onAbort, { once: true });
3456
+ handle.promise.then((value) => {
3457
+ signal.removeEventListener("abort", onAbort);
3458
+ resolve(value);
3459
+ }, (error) => {
3460
+ signal.removeEventListener("abort", onAbort);
3461
+ reject(error);
3462
+ });
3463
+ });
3464
+ }
3465
+ function getAbortMessage(reason) {
3466
+ if (reason instanceof Error && reason.message.trim().length > 0) {
3467
+ return reason.message;
3468
+ }
3469
+ if (typeof reason === "string" && reason.trim().length > 0) {
3470
+ return reason;
3471
+ }
3472
+ return "任务已取消";
3473
+ }
3474
+ function areEquivalentSessionBindings(current, next) {
3475
+ if (!current) {
3476
+ return false;
3477
+ }
3478
+ return (current.sessionId === next.sessionId &&
3479
+ current.workspaceId === next.workspaceId &&
3480
+ current.provider === next.provider &&
3481
+ current.providerSessionId === next.providerSessionId &&
3482
+ current.rawStoreRef === next.rawStoreRef &&
3483
+ current.createdAt === next.createdAt);
3484
+ }
3485
+ function areEquivalentSessionIndexRecords(current, next) {
3486
+ if (!current) {
3487
+ return false;
3488
+ }
3489
+ return (current.sessionId === next.sessionId &&
3490
+ current.workspaceId === next.workspaceId &&
3491
+ current.provider === next.provider &&
3492
+ (current.parentSessionId ?? null) === (next.parentSessionId ?? null) &&
3493
+ (current.sessionKind ?? "default") === (next.sessionKind ?? "default") &&
3494
+ (current.annotationSourceMessageId ?? null) === (next.annotationSourceMessageId ?? null) &&
3495
+ (current.annotationSourceText ?? null) === (next.annotationSourceText ?? null) &&
3496
+ (current.isSubagent ?? false) === (next.isSubagent ?? false) &&
3497
+ (current.subagentLabel ?? null) === (next.subagentLabel ?? null) &&
3498
+ current.title === next.title &&
3499
+ current.messageCount === next.messageCount &&
3500
+ current.isArchived === next.isArchived &&
3501
+ (current.lastMessageAt ?? null) === (next.lastMessageAt ?? null) &&
3502
+ current.createdAt === next.createdAt);
3503
+ }
3504
+ function areEquivalentSessionStatusSnapshots(current, next) {
3505
+ if (!current) {
3506
+ return false;
3507
+ }
3508
+ return (current.sessionId === next.sessionId &&
3509
+ current.syncStatus === next.syncStatus &&
3510
+ (current.syncCursor ?? null) === (next.syncCursor ?? null) &&
3511
+ (current.lastSyncAt ?? null) === (next.lastSyncAt ?? null) &&
3512
+ (current.lastErrorCode ?? null) === (next.lastErrorCode ?? null) &&
3513
+ (current.lastErrorDetail ?? null) === (next.lastErrorDetail ?? null) &&
3514
+ (current.resumedAt ?? null) === (next.resumedAt ?? null));
3515
+ }
3516
+ function isSqliteBusyError(error) {
3517
+ if (!error || typeof error !== "object") {
3518
+ return false;
3519
+ }
3520
+ const sqliteCode = "code" in error ? error.code : null;
3521
+ const message = error instanceof Error ? error.message : String(error);
3522
+ return sqliteCode === "SQLITE_BUSY" || message.includes("database is locked");
3523
+ }
3524
+ async function runBatchedTransactions(items, batchSize, transaction, logOptions) {
3154
3525
  const normalizedBatchSize = Math.max(1, Math.floor(batchSize) || 1);
3155
3526
  let batchCount = 0;
3156
3527
  let maxBatchMs = 0;
3157
3528
  for (let index = 0; index < items.length; index += normalizedBatchSize) {
3158
3529
  const batch = items.slice(index, index + normalizedBatchSize);
3159
3530
  const batchStartedAt = Date.now();
3160
- transaction(batch);
3531
+ let retryCount = 0;
3532
+ while (true) {
3533
+ try {
3534
+ transaction(batch);
3535
+ break;
3536
+ }
3537
+ catch (error) {
3538
+ if (!isSqliteBusyError(error) || retryCount >= SQLITE_BUSY_RETRY_LIMIT) {
3539
+ throw error;
3540
+ }
3541
+ retryCount += 1;
3542
+ await delay(SQLITE_BUSY_RETRY_DELAY_MS * retryCount);
3543
+ }
3544
+ }
3161
3545
  const batchDurationMs = Date.now() - batchStartedAt;
3546
+ const nextBatchIndex = batchCount + 1;
3547
+ if (logOptions) {
3548
+ logPerformance(logOptions.scope, batchDurationMs, {
3549
+ ...logOptions.detail,
3550
+ batchIndex: nextBatchIndex,
3551
+ batchSize: batch.length,
3552
+ batchStartIndex: index,
3553
+ retryCount,
3554
+ totalItems: items.length,
3555
+ configuredBatchSize: normalizedBatchSize
3556
+ }, {
3557
+ thresholdMs: logOptions.thresholdMs ?? SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS
3558
+ });
3559
+ }
3162
3560
  batchCount += 1;
3163
3561
  maxBatchMs = Math.max(maxBatchMs, batchDurationMs);
3164
3562
  if (index + normalizedBatchSize < items.length) {