@jingyi0605/codingns 0.9.8 → 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 (137) hide show
  1. package/dist/public/assets/{AdaptiveButlerPage-D-gXre7Y.js → AdaptiveButlerPage-CqBM1XiC.js} +2 -2
  2. package/dist/public/assets/App-Pe30N_Dy.js +30 -0
  3. package/dist/public/assets/{BootstrapPage-B-yMdfpQ.js → BootstrapPage-D9ai1XhC.js} +1 -1
  4. package/dist/public/assets/ConversationPage-C8aU99KN.js +9 -0
  5. package/dist/public/assets/{DesktopDetachPreviewPage-D1DMaGcy.js → DesktopDetachPreviewPage-BJVPVFf2.js} +1 -1
  6. package/dist/public/assets/{DesktopModal-BnfGW2gk.js → DesktopModal-DyC_e0eF.js} +1 -1
  7. package/dist/public/assets/DesktopWindowPage-BZleww0T.js +2 -0
  8. package/dist/public/assets/FileContextPanel-BklQRQXj.js +1 -0
  9. package/dist/public/assets/GitSidebar-D0tb3W87.js +6 -0
  10. package/dist/public/assets/MobileCreateSessionSheet-BS881Agk.js +1 -0
  11. package/dist/public/assets/{MobileSheet-rkn_CUOY.js → MobileSheet-C32hwIFd.js} +1 -1
  12. package/dist/public/assets/PluginAccessOverview-BDiz6fV8.js +1 -0
  13. package/dist/public/assets/{PluginContainerPage-D-ly3i3H.js → PluginContainerPage-BMT6J0YS.js} +1 -1
  14. package/dist/public/assets/{PluginDetailPage-CWAHYyyG.js → PluginDetailPage-ztNhvCWP.js} +1 -1
  15. package/dist/public/assets/{PluginsListPage-Cte3vBgR.js → PluginsListPage-Cuk2-U6E.js} +1 -1
  16. package/dist/public/assets/PureConversationPage-ShluI2En.js +1 -0
  17. package/dist/public/assets/{RelayConnectEntryPage-sRJlstx9.js → RelayConnectEntryPage-CMbjlD0k.js} +1 -1
  18. package/dist/public/assets/{ServerSettingsModal-BBft9KEC.js → ServerSettingsModal-CLSWyHu6.js} +1 -1
  19. package/dist/public/assets/SessionIndexPage-dqfauVAj.js +1 -0
  20. package/dist/public/assets/SettingsPage-PUga-wwJ.js +2 -0
  21. package/dist/public/assets/TerminalManagerPanel-75khWhRz.js +1 -0
  22. package/dist/public/assets/ToolFilesPage-S3eWxO_D.js +1 -0
  23. package/dist/public/assets/ToolGitPage-8RAMpope.js +1 -0
  24. package/dist/public/assets/ToolProcessesPage-TZACKEoH.js +1 -0
  25. package/dist/public/assets/{ToolsHomePage-D1n4FU1s.js → ToolsHomePage-Whgu6Tyf.js} +1 -1
  26. package/dist/public/assets/{WorkbenchLandingPage-BaU_dXls.js → WorkbenchLandingPage-CC24vHza.js} +1 -1
  27. package/dist/public/assets/{WorkbenchLayout-BksVkkFF.css → WorkbenchLayout-BZdfVest.css} +32 -1
  28. package/dist/public/assets/WorkbenchLayout-CoYxMzzR.js +1081 -0
  29. package/dist/public/assets/{WorkbenchModal-DWsNm2B2.js → WorkbenchModal-2PaCY_Gj.js} +1 -1
  30. package/dist/public/assets/WorkbenchShellRoute-DLVRpGzb.css +1 -0
  31. package/dist/public/assets/WorkbenchShellRoute-QXztW_Ny.js +1 -0
  32. package/dist/public/assets/WorkspaceDebugDetailPage-Ce_oMmJ8.js +1 -0
  33. package/dist/public/assets/WorkspaceDetailPage-aX6w4Q7S.js +1 -0
  34. package/dist/public/assets/WorkspaceHomePage-DQttgj2w.js +1 -0
  35. package/dist/public/assets/{client-runtime-manager-D9VbgJZ_.js → client-runtime-manager-CGVLChqs.js} +1 -1
  36. package/dist/public/assets/host-alias-CeDJeL0T.js +1 -0
  37. package/dist/public/assets/index-BehUkul4.css +1 -0
  38. package/dist/public/assets/index-Bp1FnRo_.js +50 -0
  39. package/dist/public/assets/{login-direct-candidate-resolver-17wEvjhh.js → login-direct-candidate-resolver-Bvs5uPix.js} +1 -1
  40. package/dist/public/assets/peer-host-config-sync-oelf2T0_.js +1 -0
  41. package/dist/public/assets/{plugin-permission-copy-apDn8EWG.js → plugin-permission-copy-Do028HzJ.js} +1 -1
  42. package/dist/public/assets/{plugins-api-CnZYRKoS.js → plugins-api-BOugklJ-.js} +1 -1
  43. package/dist/public/assets/{preferences-service-PZlLLAWH.js → preferences-service-Dys1mbBy.js} +1 -1
  44. package/dist/public/assets/{relay-entry-DhHwflXl.js → relay-entry-B6UDc5cD.js} +1 -1
  45. package/dist/public/assets/useRegisteredDebugTemplates-BafVuBvE.js +1 -0
  46. package/dist/public/assets/workbench-navigation-Bs4OQYD3.js +1 -0
  47. package/dist/public/index.html +2 -2
  48. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +3 -0
  49. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +9 -0
  50. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  51. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +3 -0
  52. package/dist/server/modules/assistant-capability/assistant-capability-service.js +4 -1
  53. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  54. package/dist/server/modules/sessions/session-controller.d.ts +10 -0
  55. package/dist/server/modules/sessions/session-controller.js +11 -0
  56. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  57. package/dist/server/modules/sessions/session-history-service.d.ts +42 -1
  58. package/dist/server/modules/sessions/session-history-service.js +356 -25
  59. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  60. package/dist/server/modules/tasks/observability-controller.d.ts +2 -0
  61. package/dist/server/modules/tasks/observability-controller.js +16 -1
  62. package/dist/server/modules/tasks/observability-controller.js.map +1 -1
  63. package/dist/server/modules/tasks/observability-service.d.ts +12 -2
  64. package/dist/server/modules/tasks/observability-service.js +16 -3
  65. package/dist/server/modules/tasks/observability-service.js.map +1 -1
  66. package/dist/server/modules/workbench/workbench-service.d.ts +2 -0
  67. package/dist/server/modules/workbench/workbench-service.js +162 -19
  68. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  69. package/dist/server/modules/workspace/affairs-library-controller.d.ts +1 -0
  70. package/dist/server/modules/workspace/affairs-library-controller.js +3 -0
  71. package/dist/server/modules/workspace/affairs-library-controller.js.map +1 -1
  72. package/dist/server/modules/workspace/affairs-library-service.d.ts +4 -0
  73. package/dist/server/modules/workspace/affairs-library-service.js +23 -4
  74. package/dist/server/modules/workspace/affairs-library-service.js.map +1 -1
  75. package/dist/server/modules/workspace/workspace-controller.d.ts +7 -1
  76. package/dist/server/modules/workspace/workspace-controller.js +11 -1
  77. package/dist/server/modules/workspace/workspace-controller.js.map +1 -1
  78. package/dist/server/modules/workspace/workspace-service.d.ts +9 -2
  79. package/dist/server/modules/workspace/workspace-service.js +45 -10
  80. package/dist/server/modules/workspace/workspace-service.js.map +1 -1
  81. package/dist/server/routes/affairs.js +4 -0
  82. package/dist/server/routes/affairs.js.map +1 -1
  83. package/dist/server/routes/sessions.js +2 -0
  84. package/dist/server/routes/sessions.js.map +1 -1
  85. package/dist/server/server/create-server.js +1 -1
  86. package/dist/server/server/create-server.js.map +1 -1
  87. package/dist/server/storage/repositories/session-discovery-diagnostics-repository.d.ts +10 -0
  88. package/dist/server/storage/repositories/session-discovery-diagnostics-repository.js +68 -0
  89. package/dist/server/storage/repositories/session-discovery-diagnostics-repository.js.map +1 -0
  90. package/dist/server/storage/repositories/session-source-index-repository.d.ts +14 -0
  91. package/dist/server/storage/repositories/session-source-index-repository.js +164 -0
  92. package/dist/server/storage/repositories/session-source-index-repository.js.map +1 -0
  93. package/dist/server/storage/repositories/workspace-navigation-state-repository.js +16 -7
  94. package/dist/server/storage/repositories/workspace-navigation-state-repository.js.map +1 -1
  95. package/dist/server/storage/sqlite/client.js +26 -0
  96. package/dist/server/storage/sqlite/client.js.map +1 -1
  97. package/dist/server/storage/sqlite/schema.sql +57 -0
  98. package/dist/server/types/domain.d.ts +45 -0
  99. package/node_modules/@codingns/session-sync-core/dist/providers/claude-session-store.js +19 -2
  100. package/node_modules/@codingns/session-sync-core/dist/providers/claude-session-store.js.map +1 -1
  101. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +1 -0
  102. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +45 -1
  103. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  104. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +11 -4
  105. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  106. package/package.json +1 -1
  107. package/dist/public/assets/App-Dl-mcdqy.js +0 -30
  108. package/dist/public/assets/ConversationPage-DRQ5Sg_d.js +0 -9
  109. package/dist/public/assets/DesktopWindowPage-2SWAi0xz.js +0 -2
  110. package/dist/public/assets/FileContextPanel-fbPuE9dO.js +0 -1
  111. package/dist/public/assets/GitSidebar-BkmesJJR.js +0 -6
  112. package/dist/public/assets/MobileCreateSessionSheet-CEJcDBZJ.js +0 -1
  113. package/dist/public/assets/MobileTopHeaderFrame-CU0wsYSS.js +0 -1
  114. package/dist/public/assets/MobileWorkspaceSwitcherHeader-idl8o1OB.js +0 -1
  115. package/dist/public/assets/PluginAccessOverview-BBgM6tb0.js +0 -1
  116. package/dist/public/assets/SessionIndexPage-CN7cEdl9.js +0 -1
  117. package/dist/public/assets/SettingsPage-BGT-YqG2.js +0 -2
  118. package/dist/public/assets/TerminalManagerPanel-6-ZJ8vGn.js +0 -1
  119. package/dist/public/assets/TerminalPage-6GBZ9nXN.css +0 -32
  120. package/dist/public/assets/TerminalPage-CUXXQYU2.js +0 -55
  121. package/dist/public/assets/TerminalRuntimeFallbackModal-zc3qqMKJ.js +0 -1
  122. package/dist/public/assets/ToolFilesPage-QzsZyr0F.js +0 -1
  123. package/dist/public/assets/ToolGitPage-CXg4ncuT.js +0 -1
  124. package/dist/public/assets/ToolProcessesPage-BPsOsg4w.js +0 -1
  125. package/dist/public/assets/WorkbenchLayout-DViAJhHz.js +0 -1027
  126. package/dist/public/assets/WorkbenchShellRoute-BGfRqBUa.js +0 -1
  127. package/dist/public/assets/WorkbenchShellRoute-f2jWjHWu.css +0 -1
  128. package/dist/public/assets/WorkspaceDebugDetailPage-BX0zVSsI.js +0 -1
  129. package/dist/public/assets/WorkspaceDetailPage-Dx6JX4jx.js +0 -1
  130. package/dist/public/assets/WorkspaceHomePage-DQVJ042Z.js +0 -1
  131. package/dist/public/assets/host-alias-0TfFnYxR.js +0 -1
  132. package/dist/public/assets/index-DREvg1Yu.css +0 -1
  133. package/dist/public/assets/index-FOhyOpGY.js +0 -50
  134. package/dist/public/assets/peer-host-config-sync-vYkmqzNz.js +0 -1
  135. package/dist/public/assets/terminal-runtime-meta-2zvacxvM.js +0 -1
  136. package/dist/public/assets/useRegisteredDebugTemplates-CJ-o4tFl.js +0 -1
  137. package/dist/public/assets/workbench-navigation-aqJ1ay4M.js +0 -1
@@ -7,6 +7,8 @@ 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";
12
14
  import { buildSessionTitleFromContent, normalizeRuntimePromptTitle } from "./session-title-utils.js";
@@ -72,6 +74,8 @@ export class SessionHistoryService {
72
74
  capabilityService;
73
75
  sessionActivityAuthorityService;
74
76
  sessionForkRepository;
77
+ sessionSourceIndexRepository;
78
+ sessionDiscoveryDiagnosticsRepository;
75
79
  providerSessionDeleteCli;
76
80
  claudeCodeHomeDir;
77
81
  codexModelOptionsService;
@@ -87,6 +91,7 @@ export class SessionHistoryService {
87
91
  providerControlRepository;
88
92
  taskManager;
89
93
  workspaceDiscoveryStatuses = new Map();
94
+ sessionSourceIndexRepairScopes = new Map();
90
95
  workspaceStateRefreshStatuses = new Map();
91
96
  providerCapabilityCache = new Map();
92
97
  codexDirtyBindingRepairStates = new Map();
@@ -108,6 +113,8 @@ export class SessionHistoryService {
108
113
  this.sessionMessageOriginRepository = sessionMessageOriginRepository;
109
114
  this.sessionActivityAuthorityService = sessionActivityAuthorityService;
110
115
  this.sessionForkRepository = sessionForkRepository ?? new SessionForkRepository(db);
116
+ this.sessionSourceIndexRepository = new SessionSourceIndexRepository(db);
117
+ this.sessionDiscoveryDiagnosticsRepository = new SessionDiscoveryDiagnosticsRepository(db);
111
118
  this.providerSessionDeleteCli =
112
119
  adapterOverrides.providerSessionDeleteCli ?? new CodingnsProviderSessionDeleteCli(config);
113
120
  this.taskManager = taskManager;
@@ -185,6 +192,73 @@ export class SessionHistoryService {
185
192
  observeBackgroundTaskMetrics() {
186
193
  return this.taskManager.observe();
187
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
+ }
188
262
  registerLiveActivityObservationResolver(resolver) {
189
263
  this.liveActivityObservationResolvers.add(resolver);
190
264
  let closed = false;
@@ -219,7 +293,7 @@ export class SessionHistoryService {
219
293
  this.taskManager.register({
220
294
  taskType: HOST_TASK_TYPES.workspaceDiscovery,
221
295
  executionLane: "host_background",
222
- 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)
223
297
  });
224
298
  }
225
299
  if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscoveryScan)) {
@@ -255,17 +329,14 @@ export class SessionHistoryService {
255
329
  }
256
330
  }
257
331
  async discoverWorkspaceSessions(workspaceId, userId, options) {
332
+ this.getDiscoverableWorkspaceForUserOrThrow(workspaceId, userId);
333
+ this.markWorkspaceDiscoveryRequested(workspaceId, "session_history.discover_workspace_sessions");
258
334
  const maxAgeMs = options?.maxAgeMs ?? 0;
259
335
  const force = options?.force ?? false;
260
336
  const discoveryStatus = this.workspaceDiscoveryStatuses.get(workspaceId);
261
- const lastRefreshedAt = discoveryStatus?.refreshedAt ?? 0;
262
337
  const now = Date.now();
263
- const isPartialCoolingDown = discoveryStatus?.isComplete === false &&
264
- discoveryStatus.partialCooldownUntil !== null &&
265
- now < discoveryStatus.partialCooldownUntil;
266
- const isCompleteAndFresh = discoveryStatus?.isComplete === true &&
267
- maxAgeMs > 0 &&
268
- now - lastRefreshedAt <= maxAgeMs;
338
+ const isPartialCoolingDown = this.isWorkspaceDiscoveryPartialCoolingDown(discoveryStatus, now);
339
+ const isCompleteAndFresh = this.isWorkspaceDiscoveryCompleteAndFresh(discoveryStatus, maxAgeMs, now);
269
340
  if (!force &&
270
341
  discoveryStatus &&
271
342
  (isPartialCoolingDown || isCompleteAndFresh)) {
@@ -285,6 +356,13 @@ export class SessionHistoryService {
285
356
  requestWorkspaceDiscovery(workspaceId, userId, options) {
286
357
  const maxAgeMs = options?.maxAgeMs ?? WORKSPACE_DISCOVERY_BACKGROUND_MAX_AGE_MS;
287
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
+ }
288
366
  if (!force && !this.needsWorkspaceDiscovery(workspaceId, maxAgeMs)) {
289
367
  return;
290
368
  }
@@ -312,19 +390,27 @@ export class SessionHistoryService {
312
390
  });
313
391
  }
314
392
  needsWorkspaceDiscovery(workspaceId, maxAgeMs) {
315
- if (maxAgeMs <= 0) {
316
- return true;
317
- }
318
393
  const discoveryStatus = this.workspaceDiscoveryStatuses.get(workspaceId);
319
394
  if (!discoveryStatus) {
320
395
  return true;
321
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
+ }
322
406
  if (!discoveryStatus.isComplete &&
323
- (discoveryStatus.partialCooldownUntil === null ||
324
- Date.now() >= discoveryStatus.partialCooldownUntil)) {
407
+ !this.isWorkspaceDiscoveryPartialCoolingDown(discoveryStatus, Date.now())) {
325
408
  return true;
326
409
  }
327
- return Date.now() - discoveryStatus.refreshedAt > maxAgeMs;
410
+ if (maxAgeMs <= 0) {
411
+ return true;
412
+ }
413
+ return !this.isWorkspaceDiscoveryCompleteAndFresh(discoveryStatus, maxAgeMs, Date.now());
328
414
  }
329
415
  async readSessionHistory(sessionId, cursor, limit, direction = "forward", userId) {
330
416
  const startedAt = Date.now();
@@ -1433,10 +1519,14 @@ export class SessionHistoryService {
1433
1519
  }
1434
1520
  }
1435
1521
  }
1436
- async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline", signal) {
1522
+ async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline", signal, taskId) {
1437
1523
  const startedAt = Date.now();
1438
1524
  const debugStartedAtMs = terminalDebugNowMs();
1439
- 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}`;
1440
1530
  let discoverDurationMs = 0;
1441
1531
  let persistDurationMs = 0;
1442
1532
  let persistPass1DurationMs = 0;
@@ -1450,14 +1540,16 @@ export class SessionHistoryService {
1450
1540
  let listItemsDurationMs = 0;
1451
1541
  let refreshStateDurationMs = 0;
1452
1542
  const refreshStateCount = 10;
1543
+ const activeRepairScope = this.sessionSourceIndexRepairScopes.get(workspaceId) ?? null;
1453
1544
  try {
1454
1545
  const discoverStartedAt = Date.now();
1455
1546
  const existingWorkspaceSessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
1547
+ const existingWorkspaceSourceIndexes = this.sessionSourceIndexRepository.listByWorkspaceId(workspaceId);
1456
1548
  const enabledProviders = this.providerRegistry
1457
1549
  .list()
1458
1550
  .map((adapter) => adapter.providerId)
1459
1551
  .filter((providerId) => this.isProviderEnabled(providerId));
1460
- 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);
1461
1553
  const discoveryHandle = this.taskManager.enqueue(HOST_TASK_TYPES.workspaceDiscoveryScan, {
1462
1554
  key: workspaceId,
1463
1555
  source: "session_history.workspace_discovery.scan",
@@ -1474,6 +1566,7 @@ export class SessionHistoryService {
1474
1566
  const sessions = discovery.sessions;
1475
1567
  discoverDurationMs = Date.now() - discoverStartedAt;
1476
1568
  const timestamp = nowIso();
1569
+ this.persistDiscoveryDiagnostics(workspaceId, "session_history.workspace_discovery.scan", discovery, timestamp);
1477
1570
  const discoveredSessionIds = new Map();
1478
1571
  const persistedSessions = [];
1479
1572
  const claimedPendingSessionIds = new Set();
@@ -1642,6 +1735,7 @@ export class SessionHistoryService {
1642
1735
  persistPass2DurationMs = Date.now() - persistPass2StartedAt;
1643
1736
  persistPass2BatchCount = persistPass2Stats.batchCount;
1644
1737
  persistPass2MaxBatchMs = persistPass2Stats.maxBatchMs;
1738
+ this.persistSessionSourceIndexRecords(workspaceId, workspace.path, sessions, existingWorkspaceSourceIndexes, timestamp);
1645
1739
  persistDurationMs = persistPass1DurationMs + relationMapDurationMs + persistPass2DurationMs;
1646
1740
  if (discovery.isComplete) {
1647
1741
  const cleanupStartedAt = Date.now();
@@ -1655,11 +1749,19 @@ export class SessionHistoryService {
1655
1749
  listItemsDurationMs = Date.now() - listItemsStartedAt;
1656
1750
  const refreshCandidates = buildSessionStateRefreshCandidates(items, refreshStateCount);
1657
1751
  this.workspaceDiscoveryStatuses.set(workspaceId, {
1752
+ ...discoveryStatus,
1753
+ phase: discovery.isComplete ? "fresh" : "cooldown",
1754
+ dirtyReasons: new Set(),
1658
1755
  refreshedAt: Date.now(),
1659
1756
  isComplete: discovery.isComplete,
1660
1757
  partialCooldownUntil: discovery.isComplete
1661
1758
  ? null
1662
- : 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
1663
1765
  });
1664
1766
  const refreshStateStartedAt = Date.now();
1665
1767
  if (refreshStateMode === "inline") {
@@ -1734,6 +1836,13 @@ export class SessionHistoryService {
1734
1836
  return nextItems;
1735
1837
  }
1736
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
+ });
1737
1846
  logPerformance("workspace.discover_sessions.failed", Date.now() - startedAt, {
1738
1847
  workspaceId,
1739
1848
  workspacePath: workspace.path,
@@ -1757,6 +1866,11 @@ export class SessionHistoryService {
1757
1866
  });
1758
1867
  throw error;
1759
1868
  }
1869
+ finally {
1870
+ if (activeRepairScope) {
1871
+ this.sessionSourceIndexRepairScopes.delete(workspaceId);
1872
+ }
1873
+ }
1760
1874
  }
1761
1875
  async readPage(sessionId, provider, providerSessionId, rawStoreRef, cursor, limit, direction = "forward", knownTotalMessageCount = null) {
1762
1876
  if (shouldShortCircuitClaudePendingHistory(provider, providerSessionId, rawStoreRef)) {
@@ -2450,6 +2564,65 @@ export class SessionHistoryService {
2450
2564
  }
2451
2565
  return workspace;
2452
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
+ }
2453
2626
  getBindingForUserOrThrow(sessionId, userId) {
2454
2627
  const binding = this.sessionBindingRepository.findBySessionIdForUser(sessionId, userId);
2455
2628
  if (!binding) {
@@ -3189,24 +3362,121 @@ export class SessionHistoryService {
3189
3362
  }
3190
3363
  }
3191
3364
  }
3192
- buildKnownSessionSummaries(sessions, workspacePath) {
3193
- return sessions
3194
- .filter((session) => !this.isPendingSessionAlias(session))
3195
- .filter((session) => !shouldSkipClaudePendingBinding(session))
3196
- .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
+ }
3197
3375
  const stats = safeStat(session.rawStoreRef);
3198
- return {
3376
+ const summary = {
3199
3377
  provider: session.provider,
3200
3378
  providerSessionId: session.providerSessionId,
3201
3379
  title: session.title,
3202
3380
  workspacePath,
3203
3381
  rawStoreRef: session.rawStoreRef,
3382
+ isArchived: session.isArchived,
3204
3383
  lastMessageAt: session.lastMessageAt,
3205
3384
  messageCount: session.messageCount,
3206
3385
  sourceMtimeMs: stats?.mtimeMs,
3207
3386
  sourceSizeBytes: stats?.size
3208
3387
  };
3209
- });
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
+ }
3210
3480
  }
3211
3481
  async refreshSessionState(sessionId, userId) {
3212
3482
  const binding = this.getBindingOrThrow(sessionId);
@@ -3783,6 +4053,67 @@ function pickLaterIso(left, right) {
3783
4053
  function buildProviderSessionKey(provider, providerSessionId) {
3784
4054
  return `${provider}::${providerSessionId}`;
3785
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
+ }
3786
4117
  function normalizeSessionBindingSnapshot(sessionId, snapshot) {
3787
4118
  if (snapshot.provider !== "claude-code" ||
3788
4119
  !(isPendingBindingValue(snapshot.providerSessionId) ||