@jingyi0605/codingns 0.9.7 → 0.9.9

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 (202) hide show
  1. package/dist/public/assets/{AdaptiveButlerPage-DclGPzEx.js → AdaptiveButlerPage-CqBM1XiC.js} +2 -2
  2. package/dist/public/assets/{App-CcDXqFl1.css → App-7zrCMhE-.css} +1 -1
  3. package/dist/public/assets/App-Pe30N_Dy.js +30 -0
  4. package/dist/public/assets/{BootstrapPage-Bl21SsuW.js → BootstrapPage-D9ai1XhC.js} +1 -1
  5. package/dist/public/assets/ConversationPage-C8aU99KN.js +9 -0
  6. package/dist/public/assets/{DesktopDetachPreviewPage-uaOHVsjV.js → DesktopDetachPreviewPage-BJVPVFf2.js} +1 -1
  7. package/dist/public/assets/{DesktopModal-BxsogpLf.js → DesktopModal-DyC_e0eF.js} +1 -1
  8. package/dist/public/assets/DesktopWindowPage-BZleww0T.js +2 -0
  9. package/dist/public/assets/FileContextPanel-BklQRQXj.js +1 -0
  10. package/dist/public/assets/GitSidebar-D0tb3W87.js +6 -0
  11. package/dist/public/assets/MobileCreateSessionSheet-BS881Agk.js +1 -0
  12. package/dist/public/assets/{MobileSheet-C5IVmUsO.js → MobileSheet-C32hwIFd.js} +1 -1
  13. package/dist/public/assets/PluginAccessOverview-BDiz6fV8.js +1 -0
  14. package/dist/public/assets/{PluginContainerPage-_2u-9thM.js → PluginContainerPage-BMT6J0YS.js} +1 -1
  15. package/dist/public/assets/{PluginDetailPage-F9cKjSCp.js → PluginDetailPage-ztNhvCWP.js} +1 -1
  16. package/dist/public/assets/{PluginsListPage-BOhcua_4.js → PluginsListPage-Cuk2-U6E.js} +1 -1
  17. package/dist/public/assets/PureConversationPage-ShluI2En.js +1 -0
  18. package/dist/public/assets/{RelayConnectEntryPage-Ck_uZLzj.js → RelayConnectEntryPage-CMbjlD0k.js} +1 -1
  19. package/dist/public/assets/{ServerSettingsModal-ICd82a_x.js → ServerSettingsModal-CLSWyHu6.js} +1 -1
  20. package/dist/public/assets/SessionIndexPage-dqfauVAj.js +1 -0
  21. package/dist/public/assets/SettingsPage-PUga-wwJ.js +2 -0
  22. package/dist/public/assets/TerminalManagerPanel-75khWhRz.js +1 -0
  23. package/dist/public/assets/ToolFilesPage-S3eWxO_D.js +1 -0
  24. package/dist/public/assets/ToolGitPage-8RAMpope.js +1 -0
  25. package/dist/public/assets/ToolProcessesPage-TZACKEoH.js +1 -0
  26. package/dist/public/assets/ToolsHomePage-Whgu6Tyf.js +1 -0
  27. package/dist/public/assets/WorkbenchLandingPage-CC24vHza.js +1 -0
  28. package/dist/public/assets/{WorkbenchLayout-BksVkkFF.css → WorkbenchLayout-BZdfVest.css} +32 -1
  29. package/dist/public/assets/WorkbenchLayout-CoYxMzzR.js +1081 -0
  30. package/dist/public/assets/{WorkbenchModal-CsZeLArg.js → WorkbenchModal-2PaCY_Gj.js} +1 -1
  31. package/dist/public/assets/WorkbenchShellRoute-DLVRpGzb.css +1 -0
  32. package/dist/public/assets/WorkbenchShellRoute-QXztW_Ny.js +1 -0
  33. package/dist/public/assets/WorkspaceDebugDetailPage-Ce_oMmJ8.js +1 -0
  34. package/dist/public/assets/WorkspaceDetailPage-aX6w4Q7S.js +1 -0
  35. package/dist/public/assets/WorkspaceHomePage-DQttgj2w.js +1 -0
  36. package/dist/public/assets/{client-runtime-manager-BgGugw8T.js → client-runtime-manager-CGVLChqs.js} +1 -1
  37. package/dist/public/assets/host-alias-CeDJeL0T.js +1 -0
  38. package/dist/public/assets/index-BehUkul4.css +1 -0
  39. package/dist/public/assets/index-Bp1FnRo_.js +50 -0
  40. package/dist/public/assets/{login-direct-candidate-resolver-ty2uOY5y.js → login-direct-candidate-resolver-Bvs5uPix.js} +1 -1
  41. package/dist/public/assets/peer-host-config-sync-oelf2T0_.js +1 -0
  42. package/dist/public/assets/{plugin-permission-copy-iK3faI3n.js → plugin-permission-copy-Do028HzJ.js} +1 -1
  43. package/dist/public/assets/{plugins-api-CmV7aDGA.js → plugins-api-BOugklJ-.js} +1 -1
  44. package/dist/public/assets/{preferences-service-BoSmT_ny.js → preferences-service-Dys1mbBy.js} +1 -1
  45. package/dist/public/assets/relay-entry-B6UDc5cD.js +1 -0
  46. package/dist/public/assets/useRegisteredDebugTemplates-BafVuBvE.js +1 -0
  47. package/dist/public/assets/workbench-navigation-Bs4OQYD3.js +1 -0
  48. package/dist/public/index.html +2 -2
  49. package/dist/server/middlewares/auth-guard.js +1 -0
  50. package/dist/server/middlewares/auth-guard.js.map +1 -1
  51. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +3 -0
  52. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +9 -0
  53. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  54. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +3 -0
  55. package/dist/server/modules/assistant-capability/assistant-capability-service.js +4 -1
  56. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  57. package/dist/server/modules/peer-host/host-api-proxy-service.d.ts +9 -0
  58. package/dist/server/modules/peer-host/host-api-proxy-service.js +174 -0
  59. package/dist/server/modules/peer-host/host-api-proxy-service.js.map +1 -0
  60. package/dist/server/modules/peer-host/host-handshake-controller.d.ts +7 -0
  61. package/dist/server/modules/peer-host/host-handshake-controller.js +10 -0
  62. package/dist/server/modules/peer-host/host-handshake-controller.js.map +1 -0
  63. package/dist/server/modules/peer-host/host-handshake.d.ts +15 -0
  64. package/dist/server/modules/peer-host/host-handshake.js +20 -0
  65. package/dist/server/modules/peer-host/host-handshake.js.map +1 -0
  66. package/dist/server/modules/peer-host/host-ws-proxy-service.d.ts +14 -0
  67. package/dist/server/modules/peer-host/host-ws-proxy-service.js +256 -0
  68. package/dist/server/modules/peer-host/host-ws-proxy-service.js.map +1 -0
  69. package/dist/server/modules/peer-host/peer-host-controller.d.ts +55 -0
  70. package/dist/server/modules/peer-host/peer-host-controller.js +62 -0
  71. package/dist/server/modules/peer-host/peer-host-controller.js.map +1 -0
  72. package/dist/server/modules/peer-host/peer-host-service.d.ts +77 -0
  73. package/dist/server/modules/peer-host/peer-host-service.js +529 -0
  74. package/dist/server/modules/peer-host/peer-host-service.js.map +1 -0
  75. package/dist/server/modules/sessions/codex-session-title-generator.js +18 -5
  76. package/dist/server/modules/sessions/codex-session-title-generator.js.map +1 -1
  77. package/dist/server/modules/sessions/session-controller.d.ts +10 -0
  78. package/dist/server/modules/sessions/session-controller.js +11 -0
  79. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  80. package/dist/server/modules/sessions/session-history-service.d.ts +54 -2
  81. package/dist/server/modules/sessions/session-history-service.js +407 -63
  82. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  83. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +6 -0
  84. package/dist/server/modules/sessions/session-live-runtime-service.js +181 -80
  85. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  86. package/dist/server/modules/sessions/session-title-utils.d.ts +3 -0
  87. package/dist/server/modules/sessions/session-title-utils.js +25 -0
  88. package/dist/server/modules/sessions/session-title-utils.js.map +1 -0
  89. package/dist/server/modules/sessions/workspace-office-mcp-config.d.ts +2 -1
  90. package/dist/server/modules/sessions/workspace-office-mcp-config.js +44 -3
  91. package/dist/server/modules/sessions/workspace-office-mcp-config.js.map +1 -1
  92. package/dist/server/modules/tasks/observability-controller.d.ts +2 -0
  93. package/dist/server/modules/tasks/observability-controller.js +16 -1
  94. package/dist/server/modules/tasks/observability-controller.js.map +1 -1
  95. package/dist/server/modules/tasks/observability-service.d.ts +12 -2
  96. package/dist/server/modules/tasks/observability-service.js +16 -3
  97. package/dist/server/modules/tasks/observability-service.js.map +1 -1
  98. package/dist/server/modules/tasks/task-manager.d.ts +2 -1
  99. package/dist/server/modules/tasks/task-manager.js +3 -0
  100. package/dist/server/modules/tasks/task-manager.js.map +1 -1
  101. package/dist/server/modules/tasks/task-scheduler.d.ts +2 -1
  102. package/dist/server/modules/tasks/task-scheduler.js +21 -0
  103. package/dist/server/modules/tasks/task-scheduler.js.map +1 -1
  104. package/dist/server/modules/tasks/task-types.d.ts +5 -0
  105. package/dist/server/modules/tasks/task-types.js.map +1 -1
  106. package/dist/server/modules/workbench/workbench-controller.js +3 -2
  107. package/dist/server/modules/workbench/workbench-controller.js.map +1 -1
  108. package/dist/server/modules/workbench/workbench-service.d.ts +2 -0
  109. package/dist/server/modules/workbench/workbench-service.js +162 -19
  110. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  111. package/dist/server/modules/workspace/affairs-library-controller.d.ts +1 -0
  112. package/dist/server/modules/workspace/affairs-library-controller.js +3 -0
  113. package/dist/server/modules/workspace/affairs-library-controller.js.map +1 -1
  114. package/dist/server/modules/workspace/affairs-library-service.d.ts +5 -0
  115. package/dist/server/modules/workspace/affairs-library-service.js +103 -4
  116. package/dist/server/modules/workspace/affairs-library-service.js.map +1 -1
  117. package/dist/server/modules/workspace/workspace-controller.d.ts +7 -1
  118. package/dist/server/modules/workspace/workspace-controller.js +11 -1
  119. package/dist/server/modules/workspace/workspace-controller.js.map +1 -1
  120. package/dist/server/modules/workspace/workspace-service.d.ts +9 -2
  121. package/dist/server/modules/workspace/workspace-service.js +45 -10
  122. package/dist/server/modules/workspace/workspace-service.js.map +1 -1
  123. package/dist/server/routes/affairs.js +4 -0
  124. package/dist/server/routes/affairs.js.map +1 -1
  125. package/dist/server/routes/peer-hosts.d.ts +3 -0
  126. package/dist/server/routes/peer-hosts.js +18 -0
  127. package/dist/server/routes/peer-hosts.js.map +1 -0
  128. package/dist/server/routes/public.d.ts +2 -1
  129. package/dist/server/routes/public.js +2 -1
  130. package/dist/server/routes/public.js.map +1 -1
  131. package/dist/server/routes/sessions.js +2 -0
  132. package/dist/server/routes/sessions.js.map +1 -1
  133. package/dist/server/server/create-server.d.ts +4 -0
  134. package/dist/server/server/create-server.js +31 -3
  135. package/dist/server/server/create-server.js.map +1 -1
  136. package/dist/server/shared/http/error-handler.js +12 -0
  137. package/dist/server/shared/http/error-handler.js.map +1 -1
  138. package/dist/server/storage/repositories/peer-host-repository.d.ts +44 -0
  139. package/dist/server/storage/repositories/peer-host-repository.js +271 -0
  140. package/dist/server/storage/repositories/peer-host-repository.js.map +1 -0
  141. package/dist/server/storage/repositories/session-discovery-diagnostics-repository.d.ts +10 -0
  142. package/dist/server/storage/repositories/session-discovery-diagnostics-repository.js +68 -0
  143. package/dist/server/storage/repositories/session-discovery-diagnostics-repository.js.map +1 -0
  144. package/dist/server/storage/repositories/session-source-index-repository.d.ts +14 -0
  145. package/dist/server/storage/repositories/session-source-index-repository.js +164 -0
  146. package/dist/server/storage/repositories/session-source-index-repository.js.map +1 -0
  147. package/dist/server/storage/repositories/workspace-navigation-state-repository.js +16 -7
  148. package/dist/server/storage/repositories/workspace-navigation-state-repository.js.map +1 -1
  149. package/dist/server/storage/sqlite/client.js +107 -0
  150. package/dist/server/storage/sqlite/client.js.map +1 -1
  151. package/dist/server/storage/sqlite/schema.sql +121 -0
  152. package/dist/server/types/domain.d.ts +88 -0
  153. package/dist/server/ws/workbench-ws-hub.js +5 -14
  154. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  155. package/dist/server/ws/ws-server.d.ts +2 -1
  156. package/dist/server/ws/ws-server.js +5 -1
  157. package/dist/server/ws/ws-server.js.map +1 -1
  158. package/node_modules/@codingns/session-sync-core/dist/providers/claude-session-store.js +19 -2
  159. package/node_modules/@codingns/session-sync-core/dist/providers/claude-session-store.js.map +1 -1
  160. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +1 -0
  161. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +74 -6
  162. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  163. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +11 -4
  164. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  165. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +8 -1
  166. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  167. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +3 -0
  168. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +144 -27
  169. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  170. package/package.json +1 -1
  171. package/dist/public/assets/App-BxX5mm9o.js +0 -30
  172. package/dist/public/assets/ConversationPage-CmiVCV0q.js +0 -9
  173. package/dist/public/assets/DesktopWindowPage-Bubfw1nC.js +0 -2
  174. package/dist/public/assets/FileContextPanel-DrYWcTkp.js +0 -1
  175. package/dist/public/assets/GitSidebar-CQsfJqun.js +0 -6
  176. package/dist/public/assets/MobileCreateSessionSheet-CqtmfVNo.js +0 -1
  177. package/dist/public/assets/MobileTopHeaderFrame-EoBm3-X0.js +0 -1
  178. package/dist/public/assets/MobileWorkspaceSwitcherHeader-BC8MMQs_.js +0 -1
  179. package/dist/public/assets/PluginAccessOverview-CrQiQxxZ.js +0 -1
  180. package/dist/public/assets/SessionIndexPage-Cox6P6dw.js +0 -1
  181. package/dist/public/assets/SettingsPage-BI5cM3j5.js +0 -2
  182. package/dist/public/assets/TerminalManagerPanel-DiVhBQhf.js +0 -1
  183. package/dist/public/assets/TerminalPage-6GBZ9nXN.css +0 -32
  184. package/dist/public/assets/TerminalPage-DUMUO7Ng.js +0 -55
  185. package/dist/public/assets/TerminalRuntimeFallbackModal-DGPR_CMh.js +0 -1
  186. package/dist/public/assets/ToolFilesPage-m88CAp-r.js +0 -1
  187. package/dist/public/assets/ToolGitPage-DCYpfTsb.js +0 -1
  188. package/dist/public/assets/ToolProcessesPage-Bk5ulsyq.js +0 -1
  189. package/dist/public/assets/ToolsHomePage-BrTwfjI7.js +0 -1
  190. package/dist/public/assets/WorkbenchLandingPage-q4AAmdMV.js +0 -1
  191. package/dist/public/assets/WorkbenchLayout-DD8b-m2G.js +0 -1027
  192. package/dist/public/assets/WorkbenchShellRoute-BaiW_vfb.js +0 -1
  193. package/dist/public/assets/WorkbenchShellRoute-CxKYZ6uF.css +0 -1
  194. package/dist/public/assets/WorkspaceDebugDetailPage-BcUUDEyw.js +0 -1
  195. package/dist/public/assets/WorkspaceDetailPage-zEZ1VARA.js +0 -1
  196. package/dist/public/assets/WorkspaceHomePage-CR0rqS4e.js +0 -1
  197. package/dist/public/assets/index-CFyk1rgJ.js +0 -50
  198. package/dist/public/assets/index-CrU73EIV.css +0 -1
  199. package/dist/public/assets/relay-entry-C8k5qsl5.js +0 -1
  200. package/dist/public/assets/terminal-runtime-meta-okQIDzfA.js +0 -1
  201. package/dist/public/assets/useRegisteredDebugTemplates-B_vXUtSe.js +0 -1
  202. package/dist/public/assets/workbench-navigation-DoDaQR4z.js +0 -1
@@ -7,8 +7,11 @@ import { createId } from "../../shared/utils/id.js";
7
7
  import { logPerformance } from "../../shared/utils/perf-log.js";
8
8
  import { isTerminalDebugEnabled, logTerminalDebug, terminalDebugNowMs } from "../../shared/utils/terminal-debug-log.js";
9
9
  import { nowIso } from "../../shared/utils/time.js";
10
+ import { SessionDiscoveryDiagnosticsRepository } from "../../storage/repositories/session-discovery-diagnostics-repository.js";
11
+ import { SessionSourceIndexRepository } from "../../storage/repositories/session-source-index-repository.js";
10
12
  import { inspectSessionActivity } from "./session-activity-inspector.js";
11
13
  import { SessionActivityAuthorityService } from "./session-activity-authority-service.js";
14
+ import { buildSessionTitleFromContent, normalizeRuntimePromptTitle } from "./session-title-utils.js";
12
15
  import { mapSessionProviderError } from "./session-provider-error-mapper.js";
13
16
  import { SessionForkRepository } from "../../storage/repositories/session-fork-repository.js";
14
17
  import { buildParallelGroupColorToken, resolveParallelDisplayParentSessionId } from "../parallel-sessions/parallel-session-group-service.js";
@@ -71,6 +74,8 @@ export class SessionHistoryService {
71
74
  capabilityService;
72
75
  sessionActivityAuthorityService;
73
76
  sessionForkRepository;
77
+ sessionSourceIndexRepository;
78
+ sessionDiscoveryDiagnosticsRepository;
74
79
  providerSessionDeleteCli;
75
80
  claudeCodeHomeDir;
76
81
  codexModelOptionsService;
@@ -86,11 +91,13 @@ export class SessionHistoryService {
86
91
  providerControlRepository;
87
92
  taskManager;
88
93
  workspaceDiscoveryStatuses = new Map();
94
+ sessionSourceIndexRepairScopes = new Map();
89
95
  workspaceStateRefreshStatuses = new Map();
90
96
  providerCapabilityCache = new Map();
91
97
  codexDirtyBindingRepairStates = new Map();
92
98
  streamingDeltaSuppressionDebugState = new Map();
93
99
  liveActivityObservationResolvers = new Set();
100
+ sessionTitleChangedObservers = new Set();
94
101
  sessionDeletedObservers = new Set();
95
102
  workspaceSessionRelations = new Map();
96
103
  workspaceStateRefreshTaskSequence = 0;
@@ -106,6 +113,8 @@ export class SessionHistoryService {
106
113
  this.sessionMessageOriginRepository = sessionMessageOriginRepository;
107
114
  this.sessionActivityAuthorityService = sessionActivityAuthorityService;
108
115
  this.sessionForkRepository = sessionForkRepository ?? new SessionForkRepository(db);
116
+ this.sessionSourceIndexRepository = new SessionSourceIndexRepository(db);
117
+ this.sessionDiscoveryDiagnosticsRepository = new SessionDiscoveryDiagnosticsRepository(db);
109
118
  this.providerSessionDeleteCli =
110
119
  adapterOverrides.providerSessionDeleteCli ?? new CodingnsProviderSessionDeleteCli(config);
111
120
  this.taskManager = taskManager;
@@ -183,6 +192,73 @@ export class SessionHistoryService {
183
192
  observeBackgroundTaskMetrics() {
184
193
  return this.taskManager.observe();
185
194
  }
195
+ listWorkspaceDiscoveryDiagnostics(workspaceId, userId, limit = 20) {
196
+ this.getDiscoverableWorkspaceForUserOrThrow(workspaceId, userId);
197
+ const normalizedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
198
+ return this.sessionDiscoveryDiagnosticsRepository.listByWorkspaceId(workspaceId, normalizedLimit);
199
+ }
200
+ getWorkspaceDiscoveryStatusSummary(workspaceId) {
201
+ const status = this.workspaceDiscoveryStatuses.get(workspaceId);
202
+ if (!status) {
203
+ return null;
204
+ }
205
+ return {
206
+ phase: status.phase,
207
+ dirtyReasonCount: status.dirtyReasons.size,
208
+ isComplete: status.isComplete,
209
+ lastRequestedAt: status.lastRequestedAt,
210
+ lastStartedAt: status.lastStartedAt,
211
+ lastCompletedAt: status.lastCompletedAt,
212
+ lastFailedAt: status.lastFailedAt,
213
+ nextAllowedAt: status.nextAllowedAt
214
+ };
215
+ }
216
+ async repairSessionSourceIndex(input) {
217
+ const workspaceId = input.workspaceId.trim();
218
+ const userId = input.userId.trim();
219
+ this.getDiscoverableWorkspaceForUserOrThrow(workspaceId, userId);
220
+ const provider = normalizeOptionalText(input.provider);
221
+ if (provider && !this.providerRegistry.list().some((adapter) => adapter.providerId === provider)) {
222
+ throw new AppError({
223
+ statusCode: 400,
224
+ errorCode: "INVALID_INPUT",
225
+ detail: "provider 不存在或当前不受支持",
226
+ field: "provider"
227
+ });
228
+ }
229
+ const sourceKeys = normalizeDistinctTexts(input.sourceKeys);
230
+ const rawStoreRefs = normalizeDistinctTexts(input.rawStoreRefs);
231
+ const existingRecords = this.sessionSourceIndexRepository.listByWorkspaceId(workspaceId);
232
+ const matchedRecords = existingRecords.filter((record) => matchesSessionSourceIndexRepairScope(record, provider, sourceKeys, rawStoreRefs));
233
+ const deletedSourceKeys = matchedRecords.map((record) => record.sourceKey);
234
+ const clearedSourceCount = this.sessionSourceIndexRepository.deleteBySourceKeys(deletedSourceKeys);
235
+ this.sessionSourceIndexRepairScopes.set(workspaceId, {
236
+ provider,
237
+ sourceKeys: new Set(sourceKeys),
238
+ rawStoreRefs: new Set(rawStoreRefs)
239
+ });
240
+ const awaitDiscovery = input.awaitDiscovery === true;
241
+ if (awaitDiscovery) {
242
+ await this.discoverWorkspaceSessions(workspaceId, userId, {
243
+ force: true,
244
+ refreshStateMode: "deferred"
245
+ });
246
+ }
247
+ else {
248
+ this.requestWorkspaceDiscovery(workspaceId, userId, {
249
+ force: true,
250
+ refreshStateMode: "deferred"
251
+ });
252
+ }
253
+ return {
254
+ workspaceId,
255
+ provider,
256
+ sourceKeyCount: sourceKeys.length,
257
+ rawStoreRefCount: rawStoreRefs.length,
258
+ clearedSourceCount,
259
+ awaitDiscovery
260
+ };
261
+ }
186
262
  registerLiveActivityObservationResolver(resolver) {
187
263
  this.liveActivityObservationResolvers.add(resolver);
188
264
  let closed = false;
@@ -204,12 +280,20 @@ export class SessionHistoryService {
204
280
  }
205
281
  };
206
282
  }
283
+ registerSessionTitleChangedObserver(observer) {
284
+ this.sessionTitleChangedObservers.add(observer);
285
+ return {
286
+ close: () => {
287
+ this.sessionTitleChangedObservers.delete(observer);
288
+ }
289
+ };
290
+ }
207
291
  registerBackgroundTasks() {
208
292
  if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscovery)) {
209
293
  this.taskManager.register({
210
294
  taskType: HOST_TASK_TYPES.workspaceDiscovery,
211
295
  executionLane: "host_background",
212
- run: async ({ workspaceId, userId, refreshStateMode }, context) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode, context.signal)
296
+ run: async ({ workspaceId, userId, refreshStateMode }, context) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode, context.signal, context.taskId)
213
297
  });
214
298
  }
215
299
  if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscoveryScan)) {
@@ -238,23 +322,21 @@ export class SessionHistoryService {
238
322
  this.taskManager.register({
239
323
  taskType: HOST_TASK_TYPES.sessionCodexTitleGenerate,
240
324
  executionLane: "external_process",
325
+ concurrency: 1,
241
326
  timeoutMs: 45_000,
242
- run: async ({ sessionId }, context) => this.runCodexSessionTitleGeneration(sessionId, context.signal)
327
+ run: async ({ sessionId, firstUserMessage }, context) => this.runCodexSessionTitleGeneration(sessionId, firstUserMessage, context.signal)
243
328
  });
244
329
  }
245
330
  }
246
331
  async discoverWorkspaceSessions(workspaceId, userId, options) {
332
+ this.getDiscoverableWorkspaceForUserOrThrow(workspaceId, userId);
333
+ this.markWorkspaceDiscoveryRequested(workspaceId, "session_history.discover_workspace_sessions");
247
334
  const maxAgeMs = options?.maxAgeMs ?? 0;
248
335
  const force = options?.force ?? false;
249
336
  const discoveryStatus = this.workspaceDiscoveryStatuses.get(workspaceId);
250
- const lastRefreshedAt = discoveryStatus?.refreshedAt ?? 0;
251
337
  const now = Date.now();
252
- const isPartialCoolingDown = discoveryStatus?.isComplete === false &&
253
- discoveryStatus.partialCooldownUntil !== null &&
254
- now < discoveryStatus.partialCooldownUntil;
255
- const isCompleteAndFresh = discoveryStatus?.isComplete === true &&
256
- maxAgeMs > 0 &&
257
- now - lastRefreshedAt <= maxAgeMs;
338
+ const isPartialCoolingDown = this.isWorkspaceDiscoveryPartialCoolingDown(discoveryStatus, now);
339
+ const isCompleteAndFresh = this.isWorkspaceDiscoveryCompleteAndFresh(discoveryStatus, maxAgeMs, now);
258
340
  if (!force &&
259
341
  discoveryStatus &&
260
342
  (isPartialCoolingDown || isCompleteAndFresh)) {
@@ -274,6 +356,13 @@ export class SessionHistoryService {
274
356
  requestWorkspaceDiscovery(workspaceId, userId, options) {
275
357
  const maxAgeMs = options?.maxAgeMs ?? WORKSPACE_DISCOVERY_BACKGROUND_MAX_AGE_MS;
276
358
  const force = options?.force ?? false;
359
+ if (!this.isWorkspaceDiscoverableForUser(workspaceId, userId)) {
360
+ return;
361
+ }
362
+ const discoveryStatus = this.markWorkspaceDiscoveryRequested(workspaceId, "session_history.request_workspace_discovery");
363
+ if (!force && discoveryStatus.phase === "running") {
364
+ return;
365
+ }
277
366
  if (!force && !this.needsWorkspaceDiscovery(workspaceId, maxAgeMs)) {
278
367
  return;
279
368
  }
@@ -301,19 +390,27 @@ export class SessionHistoryService {
301
390
  });
302
391
  }
303
392
  needsWorkspaceDiscovery(workspaceId, maxAgeMs) {
304
- if (maxAgeMs <= 0) {
305
- return true;
306
- }
307
393
  const discoveryStatus = this.workspaceDiscoveryStatuses.get(workspaceId);
308
394
  if (!discoveryStatus) {
309
395
  return true;
310
396
  }
397
+ if (discoveryStatus.phase === "running") {
398
+ return false;
399
+ }
400
+ if (discoveryStatus.phase === "failed") {
401
+ return true;
402
+ }
403
+ if (discoveryStatus.dirtyReasons.size > 0) {
404
+ return true;
405
+ }
311
406
  if (!discoveryStatus.isComplete &&
312
- (discoveryStatus.partialCooldownUntil === null ||
313
- Date.now() >= discoveryStatus.partialCooldownUntil)) {
407
+ !this.isWorkspaceDiscoveryPartialCoolingDown(discoveryStatus, Date.now())) {
408
+ return true;
409
+ }
410
+ if (maxAgeMs <= 0) {
314
411
  return true;
315
412
  }
316
- return Date.now() - discoveryStatus.refreshedAt > maxAgeMs;
413
+ return !this.isWorkspaceDiscoveryCompleteAndFresh(discoveryStatus, maxAgeMs, Date.now());
317
414
  }
318
415
  async readSessionHistory(sessionId, cursor, limit, direction = "forward", userId) {
319
416
  const startedAt = Date.now();
@@ -461,7 +558,6 @@ export class SessionHistoryService {
461
558
  async syncSessionTitle(sessionId, signal) {
462
559
  const binding = this.getBindingOrThrow(sessionId);
463
560
  await this.syncSessionTitleFromProvider(sessionId, binding, signal);
464
- this.requestCodexTitleGenerationIfNeeded(sessionId, "session_history.sync_session_title");
465
561
  }
466
562
  async syncWorkspaceSessionTitles(workspaceId, userId, concurrency = 1, signal) {
467
563
  const sessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
@@ -1167,7 +1263,6 @@ export class SessionHistoryService {
1167
1263
  return null;
1168
1264
  }
1169
1265
  await this.syncSessionTitleFromProvider(sessionId, binding);
1170
- this.requestCodexTitleGenerationIfNeeded(sessionId, "session_history.read_recent_history");
1171
1266
  const snapshot = this.sessionStatusSnapshotRepository.findBySessionId(sessionId);
1172
1267
  this.upsertSnapshot(sessionId, {
1173
1268
  syncStatus: "idle",
@@ -1424,10 +1519,14 @@ export class SessionHistoryService {
1424
1519
  }
1425
1520
  }
1426
1521
  }
1427
- async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline", signal) {
1522
+ async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline", signal, taskId) {
1428
1523
  const startedAt = Date.now();
1429
1524
  const debugStartedAtMs = terminalDebugNowMs();
1430
- const workspace = this.getWorkspaceForUserOrThrow(workspaceId, userId);
1525
+ const workspace = this.getDiscoverableWorkspaceForUserOrThrow(workspaceId, userId);
1526
+ const discoveryStatus = this.getOrCreateWorkspaceDiscoveryStatus(workspaceId);
1527
+ discoveryStatus.phase = "running";
1528
+ discoveryStatus.lastStartedAt = startedAt;
1529
+ discoveryStatus.runningTaskId = taskId ?? `workspace.discovery:${workspaceId}:${startedAt}`;
1431
1530
  let discoverDurationMs = 0;
1432
1531
  let persistDurationMs = 0;
1433
1532
  let persistPass1DurationMs = 0;
@@ -1441,14 +1540,16 @@ export class SessionHistoryService {
1441
1540
  let listItemsDurationMs = 0;
1442
1541
  let refreshStateDurationMs = 0;
1443
1542
  const refreshStateCount = 10;
1543
+ const activeRepairScope = this.sessionSourceIndexRepairScopes.get(workspaceId) ?? null;
1444
1544
  try {
1445
1545
  const discoverStartedAt = Date.now();
1446
1546
  const existingWorkspaceSessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
1547
+ const existingWorkspaceSourceIndexes = this.sessionSourceIndexRepository.listByWorkspaceId(workspaceId);
1447
1548
  const enabledProviders = this.providerRegistry
1448
1549
  .list()
1449
1550
  .map((adapter) => adapter.providerId)
1450
1551
  .filter((providerId) => this.isProviderEnabled(providerId));
1451
- const knownSessions = this.buildKnownSessionSummaries(existingWorkspaceSessions.filter((session) => enabledProviders.includes(session.provider)), workspace.path);
1552
+ const knownSessions = this.buildKnownSessionSummaries(existingWorkspaceSessions.filter((session) => enabledProviders.includes(session.provider)), existingWorkspaceSourceIndexes.filter((record) => enabledProviders.includes(record.provider)), workspace.path, activeRepairScope);
1452
1553
  const discoveryHandle = this.taskManager.enqueue(HOST_TASK_TYPES.workspaceDiscoveryScan, {
1453
1554
  key: workspaceId,
1454
1555
  source: "session_history.workspace_discovery.scan",
@@ -1465,6 +1566,7 @@ export class SessionHistoryService {
1465
1566
  const sessions = discovery.sessions;
1466
1567
  discoverDurationMs = Date.now() - discoverStartedAt;
1467
1568
  const timestamp = nowIso();
1569
+ this.persistDiscoveryDiagnostics(workspaceId, "session_history.workspace_discovery.scan", discovery, timestamp);
1468
1570
  const discoveredSessionIds = new Map();
1469
1571
  const persistedSessions = [];
1470
1572
  const claimedPendingSessionIds = new Set();
@@ -1633,6 +1735,7 @@ export class SessionHistoryService {
1633
1735
  persistPass2DurationMs = Date.now() - persistPass2StartedAt;
1634
1736
  persistPass2BatchCount = persistPass2Stats.batchCount;
1635
1737
  persistPass2MaxBatchMs = persistPass2Stats.maxBatchMs;
1738
+ this.persistSessionSourceIndexRecords(workspaceId, workspace.path, sessions, existingWorkspaceSourceIndexes, timestamp);
1636
1739
  persistDurationMs = persistPass1DurationMs + relationMapDurationMs + persistPass2DurationMs;
1637
1740
  if (discovery.isComplete) {
1638
1741
  const cleanupStartedAt = Date.now();
@@ -1641,19 +1744,24 @@ export class SessionHistoryService {
1641
1744
  }
1642
1745
  this.workspaceSessionRelations.set(workspaceId, relationMap);
1643
1746
  this.observeDiscoveredProviderActivity(sessions, discoveredSessionIds, userId, timestamp);
1644
- for (const persisted of persistedSessions) {
1645
- this.requestCodexTitleGenerationIfNeeded(persisted.sessionId, "session_history.discover_workspace_sessions.codex_title");
1646
- }
1647
1747
  const listItemsStartedAt = Date.now();
1648
1748
  const items = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
1649
1749
  listItemsDurationMs = Date.now() - listItemsStartedAt;
1650
1750
  const refreshCandidates = buildSessionStateRefreshCandidates(items, refreshStateCount);
1651
1751
  this.workspaceDiscoveryStatuses.set(workspaceId, {
1752
+ ...discoveryStatus,
1753
+ phase: discovery.isComplete ? "fresh" : "cooldown",
1754
+ dirtyReasons: new Set(),
1652
1755
  refreshedAt: Date.now(),
1653
1756
  isComplete: discovery.isComplete,
1654
1757
  partialCooldownUntil: discovery.isComplete
1655
1758
  ? null
1656
- : Date.now() + WORKSPACE_DISCOVERY_PARTIAL_COOLDOWN_MS
1759
+ : Date.now() + WORKSPACE_DISCOVERY_PARTIAL_COOLDOWN_MS,
1760
+ lastCompletedAt: Date.now(),
1761
+ nextAllowedAt: discovery.isComplete
1762
+ ? null
1763
+ : Date.now() + WORKSPACE_DISCOVERY_PARTIAL_COOLDOWN_MS,
1764
+ runningTaskId: null
1657
1765
  });
1658
1766
  const refreshStateStartedAt = Date.now();
1659
1767
  if (refreshStateMode === "inline") {
@@ -1728,6 +1836,13 @@ export class SessionHistoryService {
1728
1836
  return nextItems;
1729
1837
  }
1730
1838
  catch (error) {
1839
+ const failedAt = Date.now();
1840
+ this.workspaceDiscoveryStatuses.set(workspaceId, {
1841
+ ...discoveryStatus,
1842
+ phase: "failed",
1843
+ lastFailedAt: failedAt,
1844
+ runningTaskId: null
1845
+ });
1731
1846
  logPerformance("workspace.discover_sessions.failed", Date.now() - startedAt, {
1732
1847
  workspaceId,
1733
1848
  workspacePath: workspace.path,
@@ -1751,6 +1866,11 @@ export class SessionHistoryService {
1751
1866
  });
1752
1867
  throw error;
1753
1868
  }
1869
+ finally {
1870
+ if (activeRepairScope) {
1871
+ this.sessionSourceIndexRepairScopes.delete(workspaceId);
1872
+ }
1873
+ }
1754
1874
  }
1755
1875
  async readPage(sessionId, provider, providerSessionId, rawStoreRef, cursor, limit, direction = "forward", knownTotalMessageCount = null) {
1756
1876
  if (shouldShortCircuitClaudePendingHistory(provider, providerSessionId, rawStoreRef)) {
@@ -2221,7 +2341,6 @@ export class SessionHistoryService {
2221
2341
  return;
2222
2342
  }
2223
2343
  await this.syncSessionTitleFromProvider(sessionId, binding);
2224
- this.requestCodexTitleGenerationIfNeeded(sessionId, "session_history.publish_history_envelope");
2225
2344
  const snapshot = this.sessionStatusSnapshotRepository.findBySessionId(sessionId);
2226
2345
  this.upsertSnapshot(sessionId, {
2227
2346
  syncStatus: "idle",
@@ -2274,15 +2393,20 @@ export class SessionHistoryService {
2274
2393
  updatedAt: nowIso()
2275
2394
  });
2276
2395
  }
2277
- requestCodexTitleGenerationIfNeeded(sessionId, source) {
2278
- if (!this.shouldRequestCodexTitleGeneration(sessionId)) {
2396
+ requestCodexTitleGenerationForNewSession(sessionId, firstUserMessage) {
2397
+ const normalizedFirstUserMessage = firstUserMessage.trim();
2398
+ if (!normalizedFirstUserMessage) {
2399
+ return;
2400
+ }
2401
+ if (!this.shouldRequestCodexTitleGeneration(sessionId, normalizedFirstUserMessage)) {
2279
2402
  return;
2280
2403
  }
2281
2404
  const handle = this.taskManager.enqueue(HOST_TASK_TYPES.sessionCodexTitleGenerate, {
2282
2405
  key: sessionId,
2283
- source,
2406
+ source: "session_history.new_codex_session_title",
2284
2407
  input: {
2285
- sessionId
2408
+ sessionId,
2409
+ firstUserMessage: normalizedFirstUserMessage
2286
2410
  }
2287
2411
  });
2288
2412
  if (handle.deduped) {
@@ -2298,7 +2422,7 @@ export class SessionHistoryService {
2298
2422
  });
2299
2423
  });
2300
2424
  }
2301
- shouldRequestCodexTitleGeneration(sessionId) {
2425
+ shouldRequestCodexTitleGeneration(sessionId, firstUserMessage) {
2302
2426
  const binding = this.sessionBindingRepository.findBySessionId(sessionId);
2303
2427
  const index = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
2304
2428
  if (!binding || !index || binding.provider !== "codex") {
@@ -2307,12 +2431,9 @@ export class SessionHistoryService {
2307
2431
  if (isPendingBindingValue(binding.providerSessionId) || isPendingBindingValue(binding.rawStoreRef)) {
2308
2432
  return false;
2309
2433
  }
2310
- if (index.messageCount <= 0) {
2311
- return false;
2312
- }
2313
- return true;
2434
+ return shouldGenerateCodexSessionTitle(index.title, firstUserMessage);
2314
2435
  }
2315
- async runCodexSessionTitleGeneration(sessionId, signal) {
2436
+ async runCodexSessionTitleGeneration(sessionId, firstUserMessage, signal) {
2316
2437
  const binding = this.sessionBindingRepository.findBySessionId(sessionId);
2317
2438
  const index = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
2318
2439
  if (!binding || !index || binding.provider !== "codex") {
@@ -2321,20 +2442,17 @@ export class SessionHistoryService {
2321
2442
  if (isPendingBindingValue(binding.providerSessionId) || isPendingBindingValue(binding.rawStoreRef)) {
2322
2443
  return { title: null };
2323
2444
  }
2324
- const page = await this.sessionSyncService.readHistory(binding.provider, binding.providerSessionId, binding.rawStoreRef, null, 16, "forward").catch(() => null);
2325
- if (!page) {
2326
- return { title: null };
2327
- }
2328
- const firstUserMessage = page.messages.find((message) => message.role === "user")?.content ?? null;
2329
2445
  if (!shouldGenerateCodexSessionTitle(index.title, firstUserMessage)) {
2330
2446
  return { title: null };
2331
2447
  }
2332
2448
  const title = await this.codexSessionTitleGenerator.generate({
2333
2449
  currentTitle: index.title,
2334
- messages: page.messages.map((message) => ({
2335
- role: message.role,
2336
- content: message.content
2337
- })),
2450
+ messages: [
2451
+ {
2452
+ role: "user",
2453
+ content: firstUserMessage
2454
+ }
2455
+ ],
2338
2456
  signal
2339
2457
  });
2340
2458
  if (!title || !shouldApplyGeneratedCodexSessionTitle(title, firstUserMessage)) {
@@ -2348,9 +2466,29 @@ export class SessionHistoryService {
2348
2466
  title,
2349
2467
  updatedAt: nowIso()
2350
2468
  });
2469
+ await this.notifySessionTitleChanged({
2470
+ sessionId,
2471
+ userId: binding.userId,
2472
+ workspaceId: binding.workspaceId,
2473
+ title
2474
+ });
2351
2475
  }
2352
2476
  return { title };
2353
2477
  }
2478
+ async notifySessionTitleChanged(input) {
2479
+ const userId = input.userId ?? this.workspaceRepository.findById(input.workspaceId)?.ownerUserId;
2480
+ if (!userId) {
2481
+ return;
2482
+ }
2483
+ await Promise.allSettled(Array.from(this.sessionTitleChangedObservers).map(async (observer) => {
2484
+ await observer({
2485
+ sessionId: input.sessionId,
2486
+ userId,
2487
+ workspaceId: input.workspaceId,
2488
+ title: input.title
2489
+ });
2490
+ }));
2491
+ }
2354
2492
  async readFirstUserMessageTitleForSync(binding) {
2355
2493
  const pageSize = 20;
2356
2494
  const maxPages = 3;
@@ -2426,6 +2564,65 @@ export class SessionHistoryService {
2426
2564
  }
2427
2565
  return workspace;
2428
2566
  }
2567
+ getOrCreateWorkspaceDiscoveryStatus(workspaceId) {
2568
+ const existing = this.workspaceDiscoveryStatuses.get(workspaceId);
2569
+ if (existing) {
2570
+ return existing;
2571
+ }
2572
+ const created = {
2573
+ phase: "fresh",
2574
+ dirtyReasons: new Set(),
2575
+ refreshedAt: 0,
2576
+ isComplete: false,
2577
+ partialCooldownUntil: null,
2578
+ lastRequestedAt: null,
2579
+ lastStartedAt: null,
2580
+ lastCompletedAt: null,
2581
+ lastFailedAt: null,
2582
+ nextAllowedAt: null,
2583
+ runningTaskId: null
2584
+ };
2585
+ this.workspaceDiscoveryStatuses.set(workspaceId, created);
2586
+ return created;
2587
+ }
2588
+ markWorkspaceDiscoveryRequested(workspaceId, reason) {
2589
+ const status = this.getOrCreateWorkspaceDiscoveryStatus(workspaceId);
2590
+ status.lastRequestedAt = Date.now();
2591
+ if (status.phase === "running") {
2592
+ status.dirtyReasons.add(reason);
2593
+ }
2594
+ return status;
2595
+ }
2596
+ isWorkspaceDiscoveryPartialCoolingDown(status, now) {
2597
+ return Boolean(status
2598
+ && status.isComplete === false
2599
+ && status.partialCooldownUntil !== null
2600
+ && now < status.partialCooldownUntil);
2601
+ }
2602
+ isWorkspaceDiscoveryCompleteAndFresh(status, maxAgeMs, now) {
2603
+ if (!status || !status.isComplete || maxAgeMs <= 0) {
2604
+ return false;
2605
+ }
2606
+ return now - status.refreshedAt <= maxAgeMs;
2607
+ }
2608
+ isWorkspaceDiscoverableForUser(workspaceId, userId) {
2609
+ const workspace = this.workspaceRepository.findById(workspaceId);
2610
+ if (!workspace || workspace.removedAt || workspace.ownerUserId !== userId) {
2611
+ return false;
2612
+ }
2613
+ return true;
2614
+ }
2615
+ getDiscoverableWorkspaceForUserOrThrow(workspaceId, userId) {
2616
+ const workspace = this.getWorkspaceForUserOrThrow(workspaceId, userId);
2617
+ if (workspace.removedAt) {
2618
+ throw new AppError({
2619
+ statusCode: 404,
2620
+ errorCode: "WORKSPACE_NOT_FOUND",
2621
+ detail: "工作区不存在"
2622
+ });
2623
+ }
2624
+ return workspace;
2625
+ }
2429
2626
  getBindingForUserOrThrow(sessionId, userId) {
2430
2627
  const binding = this.sessionBindingRepository.findBySessionIdForUser(sessionId, userId);
2431
2628
  if (!binding) {
@@ -3165,24 +3362,121 @@ export class SessionHistoryService {
3165
3362
  }
3166
3363
  }
3167
3364
  }
3168
- buildKnownSessionSummaries(sessions, workspacePath) {
3169
- return sessions
3170
- .filter((session) => !this.isPendingSessionAlias(session))
3171
- .filter((session) => !shouldSkipClaudePendingBinding(session))
3172
- .map((session) => {
3365
+ buildKnownSessionSummaries(sessions, sourceIndexes, workspacePath, repairScope = null) {
3366
+ const merged = new Map();
3367
+ for (const session of sessions) {
3368
+ if (this.isPendingSessionAlias(session) || shouldSkipClaudePendingBinding(session)) {
3369
+ continue;
3370
+ }
3371
+ const sourceKey = buildSessionSourceKey(session.provider, session.providerSessionId, session.rawStoreRef);
3372
+ if (shouldExcludeKnownSessionFromRepairScope(repairScope, session.provider, sourceKey, session.rawStoreRef)) {
3373
+ continue;
3374
+ }
3173
3375
  const stats = safeStat(session.rawStoreRef);
3174
- return {
3376
+ const summary = {
3175
3377
  provider: session.provider,
3176
3378
  providerSessionId: session.providerSessionId,
3177
3379
  title: session.title,
3178
3380
  workspacePath,
3179
3381
  rawStoreRef: session.rawStoreRef,
3382
+ isArchived: session.isArchived,
3180
3383
  lastMessageAt: session.lastMessageAt,
3181
3384
  messageCount: session.messageCount,
3182
3385
  sourceMtimeMs: stats?.mtimeMs,
3183
3386
  sourceSizeBytes: stats?.size
3184
3387
  };
3185
- });
3388
+ merged.set(buildKnownSessionSummaryKey(summary.provider, summary.providerSessionId, summary.rawStoreRef), summary);
3389
+ }
3390
+ for (const record of sourceIndexes) {
3391
+ if (record.deletedAt) {
3392
+ continue;
3393
+ }
3394
+ if (!record.providerSessionId || !record.rawStoreRef) {
3395
+ continue;
3396
+ }
3397
+ if (shouldExcludeKnownSessionFromRepairScope(repairScope, record.provider, record.sourceKey, record.rawStoreRef)) {
3398
+ continue;
3399
+ }
3400
+ const key = buildKnownSessionSummaryKey(record.provider, record.providerSessionId, record.rawStoreRef);
3401
+ if (merged.has(key)) {
3402
+ continue;
3403
+ }
3404
+ merged.set(key, {
3405
+ provider: record.provider,
3406
+ providerSessionId: record.providerSessionId,
3407
+ title: record.title?.trim() || record.providerSessionId,
3408
+ workspacePath: record.workspacePath?.trim() || workspacePath,
3409
+ rawStoreRef: record.rawStoreRef,
3410
+ isArchived: record.isArchivedHint ?? undefined,
3411
+ lastMessageAt: record.lastMessageAt,
3412
+ messageCount: Math.max(0, record.messageCount ?? 0),
3413
+ sourceMtimeMs: record.fingerprintMtimeMs ?? undefined,
3414
+ sourceSizeBytes: record.fingerprintSizeBytes ?? undefined
3415
+ });
3416
+ }
3417
+ return [...merged.values()];
3418
+ }
3419
+ persistSessionSourceIndexRecords(workspaceId, workspacePath, sessions, existingSourceIndexes, timestamp) {
3420
+ const existingByKey = new Map(existingSourceIndexes.map((record) => [record.sourceKey, record]));
3421
+ for (const session of sessions) {
3422
+ const sourceKind = inferSessionSourceKind(session.rawStoreRef);
3423
+ const sourceKey = buildSessionSourceKey(session.provider, session.providerSessionId, session.rawStoreRef);
3424
+ const stats = safeStat(session.rawStoreRef);
3425
+ const existing = existingByKey.get(sourceKey);
3426
+ this.sessionSourceIndexRepository.upsert({
3427
+ sourceKey,
3428
+ provider: session.provider,
3429
+ sourceKind,
3430
+ workspaceId,
3431
+ providerSessionId: session.providerSessionId,
3432
+ rawStoreRef: session.rawStoreRef,
3433
+ workspacePath,
3434
+ fingerprintMtimeMs: stats?.mtimeMs ?? existing?.fingerprintMtimeMs ?? null,
3435
+ fingerprintSizeBytes: stats?.size ?? existing?.fingerprintSizeBytes ?? null,
3436
+ fingerprintInode: existing?.fingerprintInode ?? null,
3437
+ fingerprintVersion: existing?.fingerprintVersion ?? null,
3438
+ title: session.title,
3439
+ messageCount: session.messageCount,
3440
+ lastMessageAt: session.lastMessageAt,
3441
+ isArchivedHint: session.isArchived ?? existing?.isArchivedHint ?? null,
3442
+ lastParsedAt: timestamp,
3443
+ lastVerifiedAt: timestamp,
3444
+ sampleDueAt: existing?.sampleDueAt ?? null,
3445
+ deletedAt: null,
3446
+ createdAt: existing?.createdAt ?? timestamp,
3447
+ updatedAt: timestamp
3448
+ });
3449
+ }
3450
+ }
3451
+ persistDiscoveryDiagnostics(workspaceId, triggerSource, discovery, timestamp) {
3452
+ try {
3453
+ for (const entry of discovery.providerDiagnostics ?? []) {
3454
+ const record = {
3455
+ id: createId(),
3456
+ workspaceId,
3457
+ triggerSource,
3458
+ provider: entry.provider,
3459
+ isComplete: entry.isComplete,
3460
+ status: entry.status,
3461
+ durationMs: Math.max(0, Math.round(entry.durationMs)),
3462
+ sessionCount: Math.max(0, entry.sessionCount),
3463
+ scannedFiles: Math.max(0, entry.scannedFiles ?? 0),
3464
+ skippedByFingerprint: Math.max(0, entry.skippedByMtimeSize ?? 0),
3465
+ parsedFiles: Math.max(0, entry.parsedFiles ?? 0),
3466
+ bytesRead: Math.max(0, entry.bytesRead ?? 0),
3467
+ createdAt: timestamp
3468
+ };
3469
+ this.sessionDiscoveryDiagnosticsRepository.insert(record);
3470
+ }
3471
+ }
3472
+ catch (error) {
3473
+ console.warn("[session-discovery-diagnostics-persist-failed]", {
3474
+ workspaceId,
3475
+ triggerSource,
3476
+ providerCount: discovery.providerDiagnostics?.length ?? 0,
3477
+ error
3478
+ });
3479
+ }
3186
3480
  }
3187
3481
  async refreshSessionState(sessionId, userId) {
3188
3482
  const binding = this.getBindingOrThrow(sessionId);
@@ -3759,6 +4053,67 @@ function pickLaterIso(left, right) {
3759
4053
  function buildProviderSessionKey(provider, providerSessionId) {
3760
4054
  return `${provider}::${providerSessionId}`;
3761
4055
  }
4056
+ function normalizeOptionalText(value) {
4057
+ const normalized = value?.trim();
4058
+ return normalized ? normalized : null;
4059
+ }
4060
+ function normalizeDistinctTexts(values) {
4061
+ if (!Array.isArray(values)) {
4062
+ return [];
4063
+ }
4064
+ const unique = new Set();
4065
+ for (const value of values) {
4066
+ const normalized = value?.trim();
4067
+ if (normalized) {
4068
+ unique.add(normalized);
4069
+ }
4070
+ }
4071
+ return [...unique];
4072
+ }
4073
+ function matchesSessionSourceIndexRepairScope(record, provider, sourceKeys, rawStoreRefs) {
4074
+ if (provider && record.provider !== provider) {
4075
+ return false;
4076
+ }
4077
+ if (sourceKeys.length === 0 && rawStoreRefs.length === 0) {
4078
+ return true;
4079
+ }
4080
+ return sourceKeys.includes(record.sourceKey)
4081
+ || (record.rawStoreRef ? rawStoreRefs.includes(record.rawStoreRef) : false);
4082
+ }
4083
+ function shouldExcludeKnownSessionFromRepairScope(scope, provider, sourceKey, rawStoreRef) {
4084
+ if (!scope) {
4085
+ return false;
4086
+ }
4087
+ if (scope.provider && scope.provider !== provider) {
4088
+ return false;
4089
+ }
4090
+ if (scope.sourceKeys.size === 0 && scope.rawStoreRefs.size === 0) {
4091
+ return true;
4092
+ }
4093
+ return scope.sourceKeys.has(sourceKey) || scope.rawStoreRefs.has(rawStoreRef);
4094
+ }
4095
+ function buildKnownSessionSummaryKey(provider, providerSessionId, rawStoreRef) {
4096
+ return `${provider}::${providerSessionId}::${rawStoreRef}`;
4097
+ }
4098
+ function buildSessionSourceKey(provider, providerSessionId, rawStoreRef) {
4099
+ const normalizedRawStoreRef = rawStoreRef.trim();
4100
+ if (normalizedRawStoreRef.length > 0) {
4101
+ return `${provider}:raw:${normalizedRawStoreRef}`;
4102
+ }
4103
+ return `${provider}:session:${providerSessionId}`;
4104
+ }
4105
+ function inferSessionSourceKind(rawStoreRef) {
4106
+ if (rawStoreRef.endsWith(".jsonl")) {
4107
+ return "jsonl";
4108
+ }
4109
+ if (rawStoreRef.startsWith("opencode://") || rawStoreRef.startsWith("server://")) {
4110
+ return "server_session";
4111
+ }
4112
+ if (rawStoreRef.endsWith(".sqlite") || rawStoreRef.includes(".sqlite:")) {
4113
+ return "sqlite_row";
4114
+ }
4115
+ return "index_entry";
4116
+ }
3762
4117
  function normalizeSessionBindingSnapshot(sessionId, snapshot) {
3763
4118
  if (snapshot.provider !== "claude-code" ||
3764
4119
  !(isPendingBindingValue(snapshot.providerSessionId) ||
@@ -4265,7 +4620,7 @@ function shouldMatchSessionBindingByRawStoreRef(provider) {
4265
4620
  function resolveSessionListTitle(provider, existingTitle, fallbackContent, parentTitle = null) {
4266
4621
  const normalizedExistingTitle = existingTitle?.trim() ?? "";
4267
4622
  const normalizedParentTitle = parentTitle?.trim() ?? "";
4268
- const fallbackTitle = buildUserMessageTitle(fallbackContent, normalizedExistingTitle || "继续对话");
4623
+ const fallbackTitle = buildSessionTitleFromContent(fallbackContent, normalizedExistingTitle || "继续对话");
4269
4624
  if (normalizedExistingTitle.length > 0 &&
4270
4625
  !isSyntheticCodexSessionTitle(normalizedExistingTitle) &&
4271
4626
  (normalizedParentTitle.length === 0 ||
@@ -4280,17 +4635,6 @@ function resolveSessionListTitle(provider, existingTitle, fallbackContent, paren
4280
4635
  }
4281
4636
  return normalizedExistingTitle || fallbackTitle;
4282
4637
  }
4283
- function buildUserMessageTitle(content, fallbackTitle) {
4284
- const title = content.trim().replace(/\s+/g, " ");
4285
- return title.slice(0, 48) || fallbackTitle;
4286
- }
4287
- function normalizeRuntimePromptTitle(content) {
4288
- const normalized = (typeof content === "string" ? content : "").trim().replace(/\s+/g, " ");
4289
- if (normalized.length === 0) {
4290
- return null;
4291
- }
4292
- return normalized.slice(0, 48);
4293
- }
4294
4638
  function buildRecoveredSessionTitle(provider, providerSessionId) {
4295
4639
  if (isPendingBindingValue(providerSessionId)) {
4296
4640
  return "新会话";