@jingyi0605/codingns 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/public/assets/{TerminalPage-4p6EBqrR.js → TerminalPage-BlbQuWi1.js} +1 -1
  2. package/dist/public/assets/index-1VIm8lVL.css +1 -0
  3. package/dist/public/assets/index-Dti93O2S.js +109 -0
  4. package/dist/public/index.html +2 -2
  5. package/dist/server/config/env.d.ts +1 -0
  6. package/dist/server/config/env.js +5 -1
  7. package/dist/server/config/env.js.map +1 -1
  8. package/dist/server/modules/file/file-controller.d.ts +12 -1
  9. package/dist/server/modules/file/file-controller.js +72 -1
  10. package/dist/server/modules/file/file-controller.js.map +1 -1
  11. package/dist/server/modules/file/file-preview-link-service.d.ts +22 -0
  12. package/dist/server/modules/file/file-preview-link-service.js +160 -0
  13. package/dist/server/modules/file/file-preview-link-service.js.map +1 -0
  14. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +2 -1
  15. package/dist/server/modules/sessions/codex-app-server-helper-client.js +103 -0
  16. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  17. package/dist/server/modules/sessions/codex-app-server-helper-process.js +106 -1
  18. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  19. package/dist/server/modules/sessions/session-controller.d.ts +21 -0
  20. package/dist/server/modules/sessions/session-controller.js +23 -1
  21. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  22. package/dist/server/modules/sessions/session-history-service.d.ts +34 -2
  23. package/dist/server/modules/sessions/session-history-service.js +591 -27
  24. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  25. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +5 -0
  26. package/dist/server/modules/sessions/session-live-runtime-service.js +59 -2
  27. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  28. package/dist/server/modules/sessions/session-provider-error-mapper.js +66 -0
  29. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  30. package/dist/server/routes/files.js +2 -0
  31. package/dist/server/routes/files.js.map +1 -1
  32. package/dist/server/routes/sessions.js +1 -0
  33. package/dist/server/routes/sessions.js.map +1 -1
  34. package/dist/server/server/create-server.d.ts +4 -0
  35. package/dist/server/server/create-server.js +7 -2
  36. package/dist/server/server/create-server.js.map +1 -1
  37. package/dist/server/shared/utils/command-availability.d.ts +1 -0
  38. package/dist/server/shared/utils/command-availability.js +83 -0
  39. package/dist/server/shared/utils/command-availability.js.map +1 -0
  40. package/dist/server/storage/repositories/session-fork-repository.d.ts +8 -0
  41. package/dist/server/storage/repositories/session-fork-repository.js +69 -0
  42. package/dist/server/storage/repositories/session-fork-repository.js.map +1 -0
  43. package/dist/server/storage/repositories/session-index-repository.js +40 -2
  44. package/dist/server/storage/repositories/session-index-repository.js.map +1 -1
  45. package/dist/server/storage/sqlite/client.js +107 -0
  46. package/dist/server/storage/sqlite/client.js.map +1 -1
  47. package/dist/server/storage/sqlite/schema.sql +30 -0
  48. package/dist/server/types/domain.d.ts +25 -1
  49. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +6 -1
  50. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +228 -7
  51. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  52. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +26 -1
  53. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +495 -2
  54. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  55. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +94 -5
  56. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  57. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +5 -1
  58. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +269 -3
  59. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  60. package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
  61. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +117 -17
  62. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  63. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +10 -0
  64. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +128 -8
  65. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  66. package/node_modules/@codingns/session-sync-core/dist/services.d.ts +2 -1
  67. package/node_modules/@codingns/session-sync-core/dist/services.js +55 -8
  68. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  69. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +27 -0
  70. package/package.json +1 -1
  71. package/dist/public/assets/index-CxeghocY.css +0 -1
  72. package/dist/public/assets/index-DXusStl0.js +0 -108
@@ -5,12 +5,18 @@ import { hashContent } from "../../shared/utils/hash.js";
5
5
  import { createId } from "../../shared/utils/id.js";
6
6
  import { logPerformance } from "../../shared/utils/perf-log.js";
7
7
  import { nowIso } from "../../shared/utils/time.js";
8
+ import { isCommandAvailable } from "../../shared/utils/command-availability.js";
8
9
  import { inspectSessionActivity } from "./session-activity-inspector.js";
9
10
  import { SessionActivityAuthorityService } from "./session-activity-authority-service.js";
10
11
  import { mapSessionProviderError } from "./session-provider-error-mapper.js";
12
+ import { SessionForkRepository } from "../../storage/repositories/session-fork-repository.js";
11
13
  import { enrichClaudeCapabilities } from "../provider/claude-model-options.js";
12
14
  import { CodexModelOptionsService, enrichCodexCapabilities } from "../provider/codex-model-options.js";
13
15
  import { OpenCodeModelOptionsService, enrichOpenCodeCapabilities } from "../provider/opencode-model-options.js";
16
+ import { CodexAppServerHelperClient } from "./codex-app-server-helper-client.js";
17
+ const RECONSTRUCTED_FORK_TARGET_PROVIDERS = new Set(["codex", "claude-code", "opencode"]);
18
+ const FORK_RECONSTRUCTION_PAGE_SIZE = 200;
19
+ const MAX_FORK_DEPTH = 4;
14
20
  const SESSION_START_DEFERRED_PROVIDERS = new Set([
15
21
  "codex",
16
22
  "claude-code",
@@ -33,14 +39,17 @@ export class SessionHistoryService {
33
39
  sessionSyncService;
34
40
  capabilityService;
35
41
  sessionActivityAuthorityService;
42
+ sessionForkRepository;
36
43
  claudeCodeHomeDir;
37
44
  codexModelOptionsService;
38
45
  openCodeModelOptionsService;
46
+ providerCliCommandPaths;
47
+ providerCliAvailability;
39
48
  workspaceDiscoveryStatuses = new Map();
40
49
  workspaceDiscoveryInflight = new Map();
41
50
  workspaceStateRefreshInflight = new Map();
42
51
  workspaceSessionRelations = new Map();
43
- constructor(db, workspaceRepository, sessionBindingRepository, sessionChangedFileService, sessionIndexRepository, sessionMessageAttachmentService, sessionStateRepository, sessionStatusSnapshotRepository, config, sessionActivityAuthorityService = new SessionActivityAuthorityService(), sessionMessageOriginRepository = null) {
52
+ constructor(db, workspaceRepository, sessionBindingRepository, sessionChangedFileService, sessionIndexRepository, sessionMessageAttachmentService, sessionStateRepository, sessionStatusSnapshotRepository, config, sessionActivityAuthorityService = new SessionActivityAuthorityService(), sessionMessageOriginRepository = null, sessionForkRepository = null, adapterOverrides = {}) {
44
53
  this.db = db;
45
54
  this.workspaceRepository = workspaceRepository;
46
55
  this.sessionBindingRepository = sessionBindingRepository;
@@ -51,10 +60,23 @@ export class SessionHistoryService {
51
60
  this.sessionStatusSnapshotRepository = sessionStatusSnapshotRepository;
52
61
  this.sessionMessageOriginRepository = sessionMessageOriginRepository;
53
62
  this.sessionActivityAuthorityService = sessionActivityAuthorityService;
63
+ this.sessionForkRepository = sessionForkRepository ?? new SessionForkRepository(db);
54
64
  this.claudeCodeHomeDir = config.claudeCodeHomeDir;
65
+ this.providerCliCommandPaths = {
66
+ "claude-code": process.platform === "win32" ? "claude.cmd" : "claude",
67
+ codex: config.codexCliPath,
68
+ gemini: config.geminiCliPath,
69
+ kimi: config.kimiCliPath
70
+ };
71
+ // CLI 是否可用只在 Host 启动时探测一次;后续统一读缓存,更新 CLI 后重启 Host 生效。
72
+ this.providerCliAvailability = buildProviderCliAvailabilitySnapshot(this.providerCliCommandPaths);
55
73
  this.providerRegistry = new ProviderRegistry([
56
74
  new ClaudeCodeAdapter({ homeDir: config.claudeCodeHomeDir }),
57
- new CodexAdapter({ homeDir: config.codexHomeDir }),
75
+ new CodexAdapter({
76
+ homeDir: config.codexHomeDir,
77
+ forkTransportFactory: adapterOverrides.codexForkTransportFactory
78
+ ?? createCodexForkTransportFactory(config.codexCliPath, config.codexHomeDir)
79
+ }),
58
80
  new GeminiAdapter({
59
81
  homeDir: config.geminiHomeDir,
60
82
  commandPath: config.geminiCliPath
@@ -244,7 +266,7 @@ export class SessionHistoryService {
244
266
  }
245
267
  getProviderCapabilitiesSnapshot(provider) {
246
268
  try {
247
- return this.capabilityService.getProviderCapabilities(provider);
269
+ return this.applyProviderCliAvailability(this.capabilityService.getProviderCapabilities(provider));
248
270
  }
249
271
  catch (error) {
250
272
  throw mapSessionProviderError(error);
@@ -253,7 +275,7 @@ export class SessionHistoryService {
253
275
  async getProviderCapabilities(provider, workspaceId) {
254
276
  try {
255
277
  const workspacePath = workspaceId ? this.getWorkspaceOrThrow(workspaceId).path : null;
256
- return await this.enrichProviderCapabilities(this.capabilityService.getProviderCapabilities(provider), workspacePath);
278
+ return await this.enrichProviderCapabilities(this.applyProviderCliAvailability(this.capabilityService.getProviderCapabilities(provider)), workspacePath);
257
279
  }
258
280
  catch (error) {
259
281
  throw mapSessionProviderError(error);
@@ -264,7 +286,7 @@ export class SessionHistoryService {
264
286
  const workspace = this.getWorkspaceOrThrow(binding.workspaceId);
265
287
  return this.capabilityService
266
288
  .getSessionCapabilities(binding.provider, binding.providerSessionId)
267
- .then((capabilities) => this.enrichProviderCapabilities(capabilities, workspace.path))
289
+ .then((capabilities) => this.enrichProviderCapabilities(this.applyProviderCliAvailability(capabilities), workspace.path))
268
290
  .catch((error) => {
269
291
  throw mapSessionProviderError(error);
270
292
  });
@@ -277,6 +299,41 @@ export class SessionHistoryService {
277
299
  const codexEnriched = await enrichCodexCapabilities(claudeEnriched, this.codexModelOptionsService);
278
300
  return enrichOpenCodeCapabilities(codexEnriched, this.openCodeModelOptionsService, workspacePath);
279
301
  }
302
+ applyProviderCliAvailability(capabilities) {
303
+ if (!isProviderCliBacked(capabilities.provider)) {
304
+ return capabilities;
305
+ }
306
+ if (this.providerCliAvailability[capabilities.provider]) {
307
+ return capabilities;
308
+ }
309
+ const limitation = buildProviderCliUnavailableMessage(capabilities.provider);
310
+ const limitations = capabilities.limitations.includes(limitation)
311
+ ? capabilities.limitations
312
+ : [limitation, ...capabilities.limitations];
313
+ return {
314
+ ...capabilities,
315
+ canStartSession: false,
316
+ canResumeSession: false,
317
+ canSendMessage: false,
318
+ supportsSubagents: false,
319
+ supportsInterrupt: false,
320
+ supportsSessionFork: false,
321
+ supportsNativeAgents: false,
322
+ limitations
323
+ };
324
+ }
325
+ assertProviderCapabilityEnabled(provider, capability, fallbackDetail) {
326
+ const capabilities = this.getProviderCapabilitiesSnapshot(provider);
327
+ if (capabilities[capability]) {
328
+ return;
329
+ }
330
+ throw new AppError({
331
+ statusCode: 409,
332
+ errorCode: "PROVIDER_UNAVAILABLE",
333
+ detail: capabilities.limitations[0] ?? fallbackDetail,
334
+ field: "provider"
335
+ });
336
+ }
280
337
  async getSessionContextUsage(sessionId) {
281
338
  const binding = this.getBindingOrThrow(sessionId);
282
339
  try {
@@ -288,6 +345,7 @@ export class SessionHistoryService {
288
345
  }
289
346
  async resumeSession(sessionId) {
290
347
  const binding = this.getBindingOrThrow(sessionId);
348
+ this.assertProviderCapabilityEnabled(binding.provider, "canResumeSession", "当前 provider 不支持继续会话");
291
349
  try {
292
350
  const result = await this.sessionSyncService.resumeSession(binding.provider, binding.providerSessionId, binding.rawStoreRef);
293
351
  this.upsertSnapshot(sessionId, {
@@ -311,7 +369,6 @@ export class SessionHistoryService {
311
369
  }
312
370
  }
313
371
  async startSession(input) {
314
- const workspace = this.getWorkspaceOrThrow(input.workspaceId);
315
372
  if (SESSION_START_DEFERRED_PROVIDERS.has(input.provider)) {
316
373
  throw new AppError({
317
374
  statusCode: 409,
@@ -320,6 +377,11 @@ export class SessionHistoryService {
320
377
  field: "provider"
321
378
  });
322
379
  }
380
+ return this.startSessionDirect(input);
381
+ }
382
+ async startSessionDirect(input) {
383
+ const workspace = this.getWorkspaceOrThrow(input.workspaceId);
384
+ this.assertProviderCapabilityEnabled(input.provider, "canStartSession", "当前 provider 不支持创建会话");
323
385
  try {
324
386
  const result = await this.sessionSyncService.startSession(input.provider, workspace.path, {
325
387
  initialPrompt: input.initialPrompt
@@ -340,7 +402,10 @@ export class SessionHistoryService {
340
402
  sessionId,
341
403
  workspaceId: workspace.id,
342
404
  provider: result.session.provider,
343
- parentSessionId: result.session.parentProviderSessionId ?? null,
405
+ parentSessionId: input.parentSessionId ?? result.session.parentProviderSessionId ?? null,
406
+ sessionKind: input.sessionKind ?? "default",
407
+ annotationSourceMessageId: input.annotationSourceMessageId ?? null,
408
+ annotationSourceText: input.annotationSourceText ?? null,
344
409
  isSubagent: result.session.isSubagent ?? false,
345
410
  subagentLabel: result.session.subagentLabel ?? null,
346
411
  title: result.session.title,
@@ -379,6 +444,236 @@ export class SessionHistoryService {
379
444
  throw mapSessionProviderError(error);
380
445
  }
381
446
  }
447
+ async forkSession(input) {
448
+ const binding = this.getBindingOrThrow(input.sessionId);
449
+ const workspace = this.getWorkspaceOrThrow(binding.workspaceId);
450
+ const targetProvider = input.targetProvider?.trim() || binding.provider;
451
+ this.assertProviderCapabilityEnabled(targetProvider, "canStartSession", "当前 provider 不支持 fork 创建会话");
452
+ const sourceMessageId = input.sourceType === "message"
453
+ ? input.sourceMessageId?.trim() || null
454
+ : null;
455
+ if (input.sourceType === "message" && !sourceMessageId) {
456
+ throw new AppError({
457
+ statusCode: 400,
458
+ errorCode: "INVALID_INPUT",
459
+ detail: "按消息派生会话时必须提供 sourceMessageId",
460
+ field: "sourceMessageId"
461
+ });
462
+ }
463
+ this.assertForkDepthWithinLimit(input.sessionId);
464
+ if (targetProvider !== binding.provider) {
465
+ return this.forkSessionAcrossProviders({
466
+ ...input,
467
+ targetProvider
468
+ }, binding, sourceMessageId);
469
+ }
470
+ try {
471
+ const result = await this.sessionSyncService.forkSession(binding.provider, binding.providerSessionId, workspace.path, {
472
+ rawStoreRef: binding.rawStoreRef,
473
+ sourceType: input.sourceType,
474
+ sourceMessageId,
475
+ strategy: input.strategy ?? "auto"
476
+ });
477
+ const sessionId = createId();
478
+ const timestamp = nowIso();
479
+ this.db.transaction(() => {
480
+ this.sessionBindingRepository.upsert({
481
+ sessionId,
482
+ workspaceId: workspace.id,
483
+ provider: result.session.provider,
484
+ providerSessionId: result.session.providerSessionId,
485
+ rawStoreRef: result.session.rawStoreRef,
486
+ createdAt: timestamp,
487
+ updatedAt: timestamp
488
+ });
489
+ this.sessionIndexRepository.upsert({
490
+ sessionId,
491
+ workspaceId: workspace.id,
492
+ provider: result.session.provider,
493
+ parentSessionId: input.sessionId,
494
+ sessionKind: input.sessionKind ?? "default",
495
+ annotationSourceMessageId: input.annotationSourceMessageId ?? null,
496
+ annotationSourceText: input.annotationSourceText ?? null,
497
+ isSubagent: result.session.isSubagent ?? false,
498
+ subagentLabel: result.session.subagentLabel ?? null,
499
+ title: result.session.title,
500
+ messageCount: result.session.messageCount,
501
+ isArchived: result.session.isArchived ?? false,
502
+ lastMessageAt: result.session.lastMessageAt,
503
+ createdAt: timestamp,
504
+ updatedAt: timestamp
505
+ });
506
+ this.sessionForkRepository.upsert({
507
+ sessionId,
508
+ parentSessionId: input.sessionId,
509
+ provider: result.session.provider,
510
+ forkSourceType: result.forkSourceType,
511
+ forkSourceSessionId: input.sessionId,
512
+ forkSourceMessageId: sourceMessageId,
513
+ inheritedPrefixMessageCount: result.inheritedPrefixMessageCount,
514
+ providerParentSessionId: binding.providerSessionId,
515
+ providerSourceMessageId: result.providerSourceMessageId ?? null,
516
+ forkMethod: result.forkMethod,
517
+ createdAt: timestamp
518
+ });
519
+ this.sessionStatusSnapshotRepository.upsert({
520
+ sessionId,
521
+ syncStatus: "idle",
522
+ syncCursor: null,
523
+ lastSyncAt: timestamp,
524
+ lastErrorCode: null,
525
+ lastErrorDetail: null,
526
+ resumedAt: null,
527
+ updatedAt: timestamp
528
+ });
529
+ this.sessionStateRepository.upsert({
530
+ sessionId,
531
+ userId: input.userId,
532
+ runningState: "idle",
533
+ activitySource: "none",
534
+ favorite: false,
535
+ lastEventAt: result.session.lastMessageAt,
536
+ completedAt: null,
537
+ lastSeenAt: null,
538
+ updatedAt: timestamp
539
+ });
540
+ })();
541
+ const forkedSession = this.getSessionListItemOrThrow(sessionId, input.userId);
542
+ const relationMap = this.workspaceSessionRelations.get(workspace.id)
543
+ ?? new Map();
544
+ relationMap.set(sessionId, {
545
+ parentSessionId: input.sessionId,
546
+ sessionKind: forkedSession.sessionKind ?? input.sessionKind ?? "default",
547
+ annotationSourceMessageId: forkedSession.annotationSourceMessageId ?? input.annotationSourceMessageId ?? null,
548
+ annotationSourceText: forkedSession.annotationSourceText ?? input.annotationSourceText ?? null,
549
+ isSubagent: forkedSession.isSubagent ?? false,
550
+ subagentLabel: forkedSession.subagentLabel ?? null
551
+ });
552
+ this.workspaceSessionRelations.set(workspace.id, relationMap);
553
+ return this.getSessionListItemOrThrow(sessionId, input.userId);
554
+ }
555
+ catch (error) {
556
+ throw mapSessionProviderError(error);
557
+ }
558
+ }
559
+ async forkSessionAcrossProviders(input, sourceBinding, sourceMessageId) {
560
+ if (!RECONSTRUCTED_FORK_TARGET_PROVIDERS.has(input.targetProvider)) {
561
+ throw mapSessionProviderError(new Error("FORK_TARGET_PROVIDER_NOT_SUPPORTED"));
562
+ }
563
+ const sourceIndex = this.sessionIndexRepository.findIndexRecordBySessionId(input.sessionId);
564
+ const inheritedMessages = await this.readForkSourceMessages(input.sessionId, sourceBinding, input.sourceType, sourceMessageId);
565
+ const reconstructedMessages = inheritedMessages.filter((message) => (message.role === "user" || message.role === "assistant")
566
+ && message.kind === "text"
567
+ && message.content.trim().length > 0);
568
+ const inheritedPrompt = buildReconstructedForkPrompt({
569
+ sourceProvider: sourceBinding.provider,
570
+ targetProvider: input.targetProvider,
571
+ sourceType: input.sourceType,
572
+ sourceTitle: sourceIndex?.title?.trim() || null,
573
+ messages: reconstructedMessages
574
+ });
575
+ const startedSession = await this.startSessionDirect({
576
+ workspaceId: sourceBinding.workspaceId,
577
+ userId: input.userId,
578
+ provider: input.targetProvider,
579
+ initialPrompt: inheritedPrompt,
580
+ parentSessionId: input.sessionId,
581
+ sessionKind: input.sessionKind ?? "default",
582
+ annotationSourceMessageId: input.annotationSourceMessageId ?? null,
583
+ annotationSourceText: input.annotationSourceText ?? null
584
+ });
585
+ const timestamp = nowIso();
586
+ const currentIndex = this.sessionIndexRepository.findIndexRecordBySessionId(startedSession.sessionId);
587
+ this.db.transaction(() => {
588
+ if (currentIndex) {
589
+ this.sessionIndexRepository.upsert({
590
+ ...currentIndex,
591
+ parentSessionId: input.sessionId,
592
+ sessionKind: input.sessionKind ?? currentIndex.sessionKind ?? "default",
593
+ annotationSourceMessageId: input.annotationSourceMessageId ?? currentIndex.annotationSourceMessageId ?? null,
594
+ annotationSourceText: input.annotationSourceText ?? currentIndex.annotationSourceText ?? null,
595
+ updatedAt: timestamp
596
+ });
597
+ }
598
+ this.sessionForkRepository.upsert({
599
+ sessionId: startedSession.sessionId,
600
+ parentSessionId: input.sessionId,
601
+ provider: input.targetProvider,
602
+ forkSourceType: input.sourceType,
603
+ forkSourceSessionId: input.sessionId,
604
+ forkSourceMessageId: sourceMessageId,
605
+ inheritedPrefixMessageCount: reconstructedMessages.length,
606
+ providerParentSessionId: sourceBinding.providerSessionId,
607
+ providerSourceMessageId: null,
608
+ forkMethod: input.sourceType === "session"
609
+ ? "reconstructed_session_fork"
610
+ : "reconstructed_message_fork",
611
+ createdAt: timestamp
612
+ });
613
+ })();
614
+ const relationMap = this.workspaceSessionRelations.get(sourceBinding.workspaceId)
615
+ ?? new Map();
616
+ relationMap.set(startedSession.sessionId, {
617
+ parentSessionId: input.sessionId,
618
+ sessionKind: startedSession.sessionKind ?? input.sessionKind ?? "default",
619
+ annotationSourceMessageId: startedSession.annotationSourceMessageId ?? input.annotationSourceMessageId ?? null,
620
+ annotationSourceText: startedSession.annotationSourceText ?? input.annotationSourceText ?? null,
621
+ isSubagent: startedSession.isSubagent ?? false,
622
+ subagentLabel: startedSession.subagentLabel ?? null
623
+ });
624
+ this.workspaceSessionRelations.set(sourceBinding.workspaceId, relationMap);
625
+ return this.getSessionListItemOrThrow(startedSession.sessionId, input.userId);
626
+ }
627
+ async readForkSourceMessages(sessionId, binding, sourceType, sourceMessageId) {
628
+ const messages = [];
629
+ let cursor = null;
630
+ while (true) {
631
+ const page = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, cursor, FORK_RECONSTRUCTION_PAGE_SIZE, "forward");
632
+ messages.push(...page.messages);
633
+ if (!page.nextCursor) {
634
+ break;
635
+ }
636
+ cursor = page.nextCursor;
637
+ }
638
+ if (sourceType === "session") {
639
+ return messages;
640
+ }
641
+ const targetIndex = messages.findIndex((message) => message.messageId === sourceMessageId);
642
+ if (targetIndex < 0) {
643
+ throw mapSessionProviderError(new Error("FORK_SOURCE_MESSAGE_NOT_FOUND"));
644
+ }
645
+ return messages.slice(0, targetIndex + 1);
646
+ }
647
+ assertForkDepthWithinLimit(parentSessionId) {
648
+ const nextDepth = this.getSessionForkDepth(parentSessionId) + 1;
649
+ if (nextDepth > MAX_FORK_DEPTH) {
650
+ throw new AppError({
651
+ statusCode: 409,
652
+ errorCode: "FORK_DEPTH_LIMIT_EXCEEDED",
653
+ detail: `fork 会话层级最多支持 ${MAX_FORK_DEPTH} 级`
654
+ });
655
+ }
656
+ }
657
+ getSessionForkDepth(sessionId) {
658
+ let depth = 1;
659
+ let currentSessionId = sessionId;
660
+ const visitedSessionIds = new Set();
661
+ while (currentSessionId) {
662
+ if (visitedSessionIds.has(currentSessionId)) {
663
+ return depth;
664
+ }
665
+ visitedSessionIds.add(currentSessionId);
666
+ const parentSessionId = this.sessionForkRepository.findBySessionId(currentSessionId)?.parentSessionId
667
+ ?? this.sessionIndexRepository.findIndexRecordBySessionId(currentSessionId)?.parentSessionId
668
+ ?? null;
669
+ if (!parentSessionId) {
670
+ return depth;
671
+ }
672
+ depth += 1;
673
+ currentSessionId = parentSessionId;
674
+ }
675
+ return depth;
676
+ }
382
677
  async sendMessage(sessionId, content, clientRequestId, permissionMode = null) {
383
678
  const binding = this.getBindingOrThrow(sessionId);
384
679
  const result = await this.sessionSyncService
@@ -388,14 +683,21 @@ export class SessionHistoryService {
388
683
  throw mapSessionProviderError(error);
389
684
  });
390
685
  const existing = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
686
+ const sessionFork = this.sessionForkRepository.findBySessionId(sessionId);
687
+ const parentTitle = sessionFork?.parentSessionId
688
+ ? this.sessionIndexRepository.findIndexRecordBySessionId(sessionFork.parentSessionId)?.title ?? null
689
+ : null;
391
690
  this.sessionIndexRepository.upsert({
392
691
  sessionId,
393
692
  workspaceId: binding.workspaceId,
394
693
  provider: binding.provider,
395
694
  parentSessionId: existing?.parentSessionId ?? null,
695
+ sessionKind: existing?.sessionKind ?? "default",
696
+ annotationSourceMessageId: existing?.annotationSourceMessageId ?? null,
697
+ annotationSourceText: existing?.annotationSourceText ?? null,
396
698
  isSubagent: existing?.isSubagent ?? false,
397
699
  subagentLabel: existing?.subagentLabel ?? null,
398
- title: existing?.title ?? result.message.content.slice(0, 48),
700
+ title: resolveSessionListTitle(binding.provider, existing?.title ?? null, result.message.content, parentTitle),
399
701
  messageCount: (existing?.messageCount ?? 0) + 1,
400
702
  isArchived: existing?.isArchived ?? false,
401
703
  lastMessageAt: result.message.timestamp,
@@ -493,6 +795,25 @@ export class SessionHistoryService {
493
795
  messages: page.messages
494
796
  };
495
797
  }
798
+ async readAllTextHistoryMessages(sessionId, limit = FORK_RECONSTRUCTION_PAGE_SIZE) {
799
+ const binding = this.getBindingOrThrow(sessionId);
800
+ const messages = [];
801
+ let cursor = null;
802
+ let remaining = Math.max(limit, 0);
803
+ while (remaining > 0) {
804
+ const pageSize = Math.min(remaining, FORK_RECONSTRUCTION_PAGE_SIZE);
805
+ const page = await this.readPage(sessionId, binding.provider, binding.providerSessionId, binding.rawStoreRef, cursor, pageSize, "forward");
806
+ messages.push(...page.messages.filter((message) => (message.role === "user" || message.role === "assistant")
807
+ && message.kind === "text"
808
+ && message.content.trim().length > 0));
809
+ if (!page.nextCursor || page.messages.length === 0) {
810
+ break;
811
+ }
812
+ cursor = page.nextCursor;
813
+ remaining -= page.messages.length;
814
+ }
815
+ return messages;
816
+ }
496
817
  async markSessionSeen(sessionId, userId) {
497
818
  const existing = this.sessionStateRepository.findBySessionAndUser(sessionId, userId) ??
498
819
  (await this.refreshSessionState(sessionId, userId));
@@ -556,6 +877,9 @@ export class SessionHistoryService {
556
877
  workspaceId: existing.workspaceId,
557
878
  provider: existing.provider,
558
879
  parentSessionId: existing.parentSessionId ?? null,
880
+ sessionKind: existing.sessionKind ?? "default",
881
+ annotationSourceMessageId: existing.annotationSourceMessageId ?? null,
882
+ annotationSourceText: existing.annotationSourceText ?? null,
559
883
  isSubagent: existing.isSubagent ?? false,
560
884
  subagentLabel: existing.subagentLabel ?? null,
561
885
  title: existing.title,
@@ -699,11 +1023,24 @@ export class SessionHistoryService {
699
1023
  createdAt,
700
1024
  updatedAt: timestamp
701
1025
  });
1026
+ const preservedParentSessionId = existingIndex?.parentSessionId
1027
+ ?? this.sessionForkRepository.findBySessionId(sessionId)?.parentSessionId
1028
+ ?? null;
1029
+ const preservedParentTitle = preservedParentSessionId
1030
+ ? this.sessionIndexRepository.findIndexRecordBySessionId(preservedParentSessionId)?.title ?? null
1031
+ : null;
1032
+ const preservedTitle = resolvePersistedSessionTitle(session.provider, session.title, existingIndex?.title ?? null, preservedParentTitle);
702
1033
  this.sessionIndexRepository.upsert({
703
1034
  sessionId,
704
1035
  workspaceId: workspace.id,
705
1036
  provider: session.provider,
706
- title: session.title,
1037
+ parentSessionId: preservedParentSessionId,
1038
+ sessionKind: existingIndex?.sessionKind ?? "default",
1039
+ annotationSourceMessageId: existingIndex?.annotationSourceMessageId ?? null,
1040
+ annotationSourceText: existingIndex?.annotationSourceText ?? null,
1041
+ isSubagent: existingIndex?.isSubagent ?? false,
1042
+ subagentLabel: existingIndex?.subagentLabel ?? null,
1043
+ title: preservedTitle,
707
1044
  messageCount: session.messageCount,
708
1045
  isArchived: resolveDiscoveredArchiveState(existingIndex?.isArchived ?? false, session.isArchived),
709
1046
  lastMessageAt: session.lastMessageAt,
@@ -731,14 +1068,34 @@ export class SessionHistoryService {
731
1068
  const relationMap = this.buildWorkspaceSessionRelationMap(sessions, discoveredSessionIds);
732
1069
  for (const persistedSession of persistedSessions) {
733
1070
  const relation = relationMap.get(persistedSession.sessionId);
1071
+ const resolvedParentSessionId = relation?.parentSessionId
1072
+ ?? persistedSession.existingIndex?.parentSessionId
1073
+ ?? this.sessionForkRepository.findBySessionId(persistedSession.sessionId)?.parentSessionId
1074
+ ?? null;
1075
+ const resolvedParentTitle = resolvedParentSessionId
1076
+ ? this.sessionIndexRepository.findIndexRecordBySessionId(resolvedParentSessionId)?.title ?? null
1077
+ : null;
734
1078
  this.sessionIndexRepository.upsert({
735
1079
  sessionId: persistedSession.sessionId,
736
1080
  workspaceId: workspace.id,
737
1081
  provider: persistedSession.session.provider,
738
- parentSessionId: relation?.parentSessionId ?? null,
739
- isSubagent: relation?.isSubagent ?? false,
740
- subagentLabel: relation?.subagentLabel ?? null,
741
- title: persistedSession.session.title,
1082
+ parentSessionId: resolvedParentSessionId,
1083
+ sessionKind: relation?.sessionKind
1084
+ ?? persistedSession.existingIndex?.sessionKind
1085
+ ?? "default",
1086
+ annotationSourceMessageId: relation?.annotationSourceMessageId
1087
+ ?? persistedSession.existingIndex?.annotationSourceMessageId
1088
+ ?? null,
1089
+ annotationSourceText: relation?.annotationSourceText
1090
+ ?? persistedSession.existingIndex?.annotationSourceText
1091
+ ?? null,
1092
+ isSubagent: relation?.isSubagent
1093
+ ?? persistedSession.existingIndex?.isSubagent
1094
+ ?? false,
1095
+ subagentLabel: relation?.subagentLabel
1096
+ ?? persistedSession.existingIndex?.subagentLabel
1097
+ ?? null,
1098
+ title: resolvePersistedSessionTitle(persistedSession.session.provider, persistedSession.session.title, persistedSession.existingIndex?.title ?? null, resolvedParentTitle),
742
1099
  messageCount: persistedSession.session.messageCount,
743
1100
  isArchived: resolveDiscoveredArchiveState(persistedSession.existingIndex?.isArchived ?? false, persistedSession.session.isArchived),
744
1101
  lastMessageAt: persistedSession.session.lastMessageAt,
@@ -755,16 +1112,16 @@ export class SessionHistoryService {
755
1112
  }
756
1113
  this.workspaceSessionRelations.set(workspaceId, this.buildWorkspaceSessionRelationMap(sessions, discoveredSessionIds));
757
1114
  const items = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
758
- const recentItems = items.slice(0, refreshStateCount);
1115
+ const refreshCandidates = buildSessionStateRefreshCandidates(items, refreshStateCount);
759
1116
  this.workspaceDiscoveryStatuses.set(workspaceId, {
760
1117
  refreshedAt: Date.now(),
761
1118
  isComplete: discovery.isComplete
762
1119
  });
763
1120
  if (refreshStateMode === "inline") {
764
- await this.refreshRecentSessionStates(recentItems, userId);
1121
+ await this.refreshRecentSessionStates(refreshCandidates, userId);
765
1122
  }
766
1123
  else {
767
- this.scheduleWorkspaceStateRefresh(workspaceId, userId, recentItems);
1124
+ this.scheduleWorkspaceStateRefresh(workspaceId, userId, refreshCandidates);
768
1125
  }
769
1126
  const nextItems = this.listWorkspaceSessions(workspaceId, userId);
770
1127
  logPerformance("workspace.discover_sessions", Date.now() - startedAt, {
@@ -774,7 +1131,8 @@ export class SessionHistoryService {
774
1131
  discoveredSessions: sessions.length,
775
1132
  returnedSessions: nextItems.length,
776
1133
  discoveryComplete: discovery.isComplete,
777
- refreshedStates: Math.min(items.length, refreshStateCount),
1134
+ providerDiagnostics: (discovery.providerDiagnostics ?? []).map((entry) => `${entry.provider}:${entry.status}:${Math.round(entry.durationMs)}ms`),
1135
+ refreshedStates: refreshCandidates.length,
778
1136
  discoverMs: discoverDurationMs,
779
1137
  persistMs: persistDurationMs,
780
1138
  refreshStateDeferred: refreshStateMode !== "inline"
@@ -909,11 +1267,17 @@ export class SessionHistoryService {
909
1267
  ? discoveredSessionIds.get(buildProviderSessionKey(session.provider, session.parentProviderSessionId)) ??
910
1268
  this.sessionBindingRepository.findByProviderSession(session.provider, session.parentProviderSessionId)?.sessionId ??
911
1269
  null
912
- : null;
1270
+ : this.resolvePersistedParentSessionId(sessionId);
913
1271
  relationMap.set(sessionId, {
914
1272
  parentSessionId,
915
- isSubagent: Boolean(session.isSubagent || parentSessionId),
916
- subagentLabel: session.subagentLabel?.trim() || null
1273
+ sessionKind: this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.sessionKind ?? "default",
1274
+ annotationSourceMessageId: this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.annotationSourceMessageId ?? null,
1275
+ annotationSourceText: this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.annotationSourceText ?? null,
1276
+ isSubagent: session.isSubagent === true
1277
+ || this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.isSubagent === true,
1278
+ subagentLabel: session.subagentLabel?.trim()
1279
+ || this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.subagentLabel
1280
+ || null
917
1281
  });
918
1282
  }
919
1283
  return relationMap;
@@ -931,6 +1295,9 @@ export class SessionHistoryService {
931
1295
  return this.enrichSessionItem({
932
1296
  ...item,
933
1297
  parentSessionId: relation.parentSessionId,
1298
+ sessionKind: relation.sessionKind,
1299
+ annotationSourceMessageId: relation.annotationSourceMessageId,
1300
+ annotationSourceText: relation.annotationSourceText,
934
1301
  isSubagent: relation.isSubagent,
935
1302
  subagentLabel: relation.subagentLabel
936
1303
  });
@@ -942,12 +1309,18 @@ export class SessionHistoryService {
942
1309
  ? {
943
1310
  ...item,
944
1311
  parentSessionId: relation.parentSessionId,
1312
+ sessionKind: relation.sessionKind,
1313
+ annotationSourceMessageId: relation.annotationSourceMessageId,
1314
+ annotationSourceText: relation.annotationSourceText,
945
1315
  isSubagent: relation.isSubagent,
946
1316
  subagentLabel: relation.subagentLabel
947
1317
  }
948
1318
  : {
949
1319
  ...item,
950
1320
  parentSessionId: item.parentSessionId ?? null,
1321
+ sessionKind: item.sessionKind ?? "default",
1322
+ annotationSourceMessageId: item.annotationSourceMessageId ?? null,
1323
+ annotationSourceText: item.annotationSourceText ?? null,
951
1324
  isSubagent: item.isSubagent ?? false,
952
1325
  subagentLabel: item.subagentLabel ?? null
953
1326
  };
@@ -1019,15 +1392,21 @@ export class SessionHistoryService {
1019
1392
  return;
1020
1393
  }
1021
1394
  const nextTitle = (await this.sessionSyncService.readSessionTitle(binding.provider, binding.providerSessionId, binding.rawStoreRef)).trim();
1022
- if (nextTitle.length === 0 || nextTitle === currentIndex.title) {
1395
+ const resolvedTitle = resolvePersistedSessionTitle(binding.provider, nextTitle, currentIndex.title);
1396
+ if (resolvedTitle.length === 0 || resolvedTitle === currentIndex.title) {
1023
1397
  return;
1024
1398
  }
1025
1399
  this.sessionIndexRepository.upsert({
1026
1400
  ...currentIndex,
1027
- title: nextTitle,
1401
+ title: resolvedTitle,
1028
1402
  updatedAt: nowIso()
1029
1403
  });
1030
1404
  }
1405
+ resolvePersistedParentSessionId(sessionId) {
1406
+ return (this.sessionForkRepository.findBySessionId(sessionId)?.parentSessionId
1407
+ ?? this.sessionIndexRepository.findIndexRecordBySessionId(sessionId)?.parentSessionId
1408
+ ?? null);
1409
+ }
1031
1410
  async ensureSessionChangedFilesIndexed(sessionId) {
1032
1411
  if (this.sessionChangedFileService.hasIndexedSession(sessionId)) {
1033
1412
  return;
@@ -1291,6 +1670,9 @@ export class SessionHistoryService {
1291
1670
  this.db
1292
1671
  .prepare("DELETE FROM session_status_snapshots WHERE session_id = ?")
1293
1672
  .run(input.sourceSessionId);
1673
+ this.db
1674
+ .prepare("DELETE FROM session_forks WHERE session_id = ?")
1675
+ .run(input.sourceSessionId);
1294
1676
  this.db
1295
1677
  .prepare("DELETE FROM session_indices WHERE session_id = ?")
1296
1678
  .run(input.sourceSessionId);
@@ -1362,11 +1744,25 @@ export class SessionHistoryService {
1362
1744
  relationMap.delete(sourceSessionId);
1363
1745
  relationMap.set(targetSessionId, {
1364
1746
  parentSessionId: targetRelation?.parentSessionId ?? sourceRelation?.parentSessionId ?? fallbackParentSessionId,
1747
+ sessionKind: targetRelation?.sessionKind
1748
+ ?? sourceRelation?.sessionKind
1749
+ ?? targetIndex?.sessionKind
1750
+ ?? sourceIndex?.sessionKind
1751
+ ?? "default",
1752
+ annotationSourceMessageId: targetRelation?.annotationSourceMessageId
1753
+ ?? sourceRelation?.annotationSourceMessageId
1754
+ ?? targetIndex?.annotationSourceMessageId
1755
+ ?? sourceIndex?.annotationSourceMessageId
1756
+ ?? null,
1757
+ annotationSourceText: targetRelation?.annotationSourceText
1758
+ ?? sourceRelation?.annotationSourceText
1759
+ ?? targetIndex?.annotationSourceText
1760
+ ?? sourceIndex?.annotationSourceText
1761
+ ?? null,
1365
1762
  isSubagent: Boolean(targetRelation?.isSubagent
1366
1763
  || sourceRelation?.isSubagent
1367
1764
  || targetIndex?.isSubagent
1368
- || sourceIndex?.isSubagent
1369
- || fallbackParentSessionId),
1765
+ || sourceIndex?.isSubagent),
1370
1766
  subagentLabel: targetRelation?.subagentLabel
1371
1767
  ?? sourceRelation?.subagentLabel
1372
1768
  ?? targetIndex?.subagentLabel
@@ -1396,6 +1792,9 @@ export class SessionHistoryService {
1396
1792
  this.db
1397
1793
  .prepare("DELETE FROM session_status_snapshots WHERE session_id = ?")
1398
1794
  .run(sessionId);
1795
+ this.db
1796
+ .prepare("DELETE FROM session_forks WHERE session_id = ?")
1797
+ .run(sessionId);
1399
1798
  this.db
1400
1799
  .prepare("DELETE FROM session_indices WHERE session_id = ?")
1401
1800
  .run(sessionId);
@@ -1426,17 +1825,24 @@ export class SessionHistoryService {
1426
1825
  const current = this.sessionStateRepository.findBySessionAndUser(sessionId, userId);
1427
1826
  const inspection = inspectSessionActivity(binding.provider, binding.rawStoreRef);
1428
1827
  const timestamp = nowIso();
1828
+ const nowMs = Date.parse(timestamp);
1829
+ if (shouldClearStaleRuntimeWithoutInspection(current, inspection, nowMs)) {
1830
+ this.sessionActivityAuthorityService.clearSession(sessionId);
1831
+ }
1429
1832
  if (shouldPreserveRuntimeTerminalState(current, inspection)) {
1430
1833
  return current;
1431
1834
  }
1432
1835
  const resolution = this.sessionActivityAuthorityService.observe(buildInspectionActivityObservation(sessionId, inspection, timestamp));
1836
+ const resolvedLastEventAt = hasInspectionEvidence(inspection)
1837
+ ? resolution.lastObservedAt ?? inspection.lastEventAt ?? current?.lastEventAt ?? null
1838
+ : current?.lastEventAt ?? null;
1433
1839
  const nextRecord = {
1434
1840
  sessionId,
1435
1841
  userId,
1436
1842
  runningState: mapResolvedRunningStateToStored(resolution.runningState, current),
1437
1843
  activitySource: mapResolutionSourceToLegacyActivitySource(resolution.activityResolutionSource, inspection),
1438
1844
  favorite: current?.favorite ?? false,
1439
- lastEventAt: resolution.lastObservedAt ?? inspection.lastEventAt ?? current?.lastEventAt ?? null,
1845
+ lastEventAt: resolvedLastEventAt,
1440
1846
  completedAt: isTerminalResolvedRunningState(resolution.runningState)
1441
1847
  ? resolution.terminalAt ?? inspection.completedAtCandidate ?? current?.completedAt ?? null
1442
1848
  : null,
@@ -1496,11 +1902,52 @@ export class SessionHistoryService {
1496
1902
  });
1497
1903
  }
1498
1904
  }
1905
+ function isProviderCliBacked(provider) {
1906
+ return provider === "claude-code" || provider === "codex" || provider === "gemini" || provider === "kimi";
1907
+ }
1908
+ function buildProviderCliAvailabilitySnapshot(commandPaths) {
1909
+ return Object.freeze(Object.fromEntries(Object.entries(commandPaths).map(([provider, commandPath]) => [
1910
+ provider,
1911
+ isCommandAvailable(commandPath)
1912
+ ])));
1913
+ }
1914
+ function buildProviderCliUnavailableMessage(provider) {
1915
+ switch (provider) {
1916
+ case "claude-code":
1917
+ return "未检测到 Claude CLI";
1918
+ case "codex":
1919
+ return "未检测到 Codex CLI";
1920
+ case "gemini":
1921
+ return "未检测到 Gemini CLI";
1922
+ case "kimi":
1923
+ return "未检测到 Kimi CLI";
1924
+ default:
1925
+ return "未检测到对应 CLI";
1926
+ }
1927
+ }
1928
+ function createCodexForkTransportFactory(commandPath, homeDir) {
1929
+ return () => {
1930
+ const client = new CodexAppServerHelperClient(commandPath, { homeDir });
1931
+ const transport = client.createForkTransport();
1932
+ return {
1933
+ ...transport,
1934
+ close() {
1935
+ transport.close();
1936
+ client.dispose();
1937
+ }
1938
+ };
1939
+ };
1940
+ }
1499
1941
  function buildInspectionActivityObservation(sessionId, inspection, observedAt) {
1942
+ const resolvedRunningState = inspection.runningState === "failed"
1943
+ ? "failed"
1944
+ : inspection.completedAtCandidate
1945
+ ? "completed"
1946
+ : inspection.runningState;
1500
1947
  return {
1501
1948
  sessionId,
1502
1949
  runId: null,
1503
- runningState: inspection.runningState,
1950
+ runningState: resolvedRunningState,
1504
1951
  source: hasInspectionEvidence(inspection) ? "inferred_log" : "unknown",
1505
1952
  confidence: "weak",
1506
1953
  detail: inspection.errorDetail,
@@ -1557,6 +2004,20 @@ function clampLimit(limit) {
1557
2004
  }
1558
2005
  return Math.max(1, Math.min(Math.trunc(limit), 100));
1559
2006
  }
2007
+ function buildSessionStateRefreshCandidates(items, recentCount) {
2008
+ const recentItems = items.slice(0, recentCount);
2009
+ const activeResidues = items.filter((item) => isSessionStateRefreshCandidate(item));
2010
+ const deduped = new Map();
2011
+ for (const item of [...recentItems, ...activeResidues]) {
2012
+ deduped.set(item.sessionId, item);
2013
+ }
2014
+ return Array.from(deduped.values());
2015
+ }
2016
+ function isSessionStateRefreshCandidate(item) {
2017
+ return item.activityState === "running"
2018
+ || item.runningState === "starting"
2019
+ || item.runningState === "running";
2020
+ }
1560
2021
  function mapSessionStateRecordRow(row) {
1561
2022
  return {
1562
2023
  sessionId: row.session_id,
@@ -1622,6 +2083,9 @@ function mergeSessionIndexRecord(input) {
1622
2083
  workspaceId: input.workspaceId,
1623
2084
  provider: (input.target?.provider ?? input.source?.provider ?? input.provider),
1624
2085
  parentSessionId: input.target?.parentSessionId ?? input.source?.parentSessionId ?? null,
2086
+ sessionKind: input.target?.sessionKind ?? input.source?.sessionKind ?? "default",
2087
+ annotationSourceMessageId: input.target?.annotationSourceMessageId ?? input.source?.annotationSourceMessageId ?? null,
2088
+ annotationSourceText: input.target?.annotationSourceText ?? input.source?.annotationSourceText ?? null,
1625
2089
  isSubagent: Boolean(input.target?.isSubagent || input.source?.isSubagent),
1626
2090
  subagentLabel: input.target?.subagentLabel ?? input.source?.subagentLabel ?? null,
1627
2091
  title: pickPreferredSessionTitle(input.target?.title ?? null, input.source?.title ?? null),
@@ -1991,6 +2455,56 @@ function isLegacyCodingNsRolloutSession(providerSessionId, rawStoreRef) {
1991
2455
  function shouldRemoveMissingSyntheticCodexSession(rawStoreRef) {
1992
2456
  return isSyntheticCodexRawStoreRef(rawStoreRef) && !existsSync(rawStoreRef);
1993
2457
  }
2458
+ function resolveSessionListTitle(provider, existingTitle, fallbackContent, parentTitle = null) {
2459
+ const normalizedExistingTitle = existingTitle?.trim() ?? "";
2460
+ const normalizedParentTitle = parentTitle?.trim() ?? "";
2461
+ const fallbackTitle = buildUserMessageTitle(fallbackContent, normalizedExistingTitle || "继续对话");
2462
+ if (normalizedExistingTitle.length > 0 &&
2463
+ !isSyntheticCodexSessionTitle(normalizedExistingTitle) &&
2464
+ (normalizedParentTitle.length === 0 ||
2465
+ normalizedExistingTitle !== normalizedParentTitle)) {
2466
+ return normalizedExistingTitle;
2467
+ }
2468
+ if (normalizedParentTitle.length > 0 && normalizedExistingTitle === normalizedParentTitle) {
2469
+ return fallbackTitle;
2470
+ }
2471
+ if (provider === "codex") {
2472
+ return fallbackTitle;
2473
+ }
2474
+ return normalizedExistingTitle || fallbackTitle;
2475
+ }
2476
+ function buildUserMessageTitle(content, fallbackTitle) {
2477
+ const title = content.trim().replace(/\s+/g, " ");
2478
+ return title.slice(0, 48) || fallbackTitle;
2479
+ }
2480
+ function resolvePersistedSessionTitle(provider, discoveredTitle, existingTitle, parentTitle = null) {
2481
+ const nextTitle = discoveredTitle.trim();
2482
+ const currentTitle = existingTitle?.trim() ?? "";
2483
+ const normalizedParentTitle = parentTitle?.trim() ?? "";
2484
+ if (!currentTitle) {
2485
+ if (provider === "codex" && isSyntheticCodexSessionTitle(nextTitle)) {
2486
+ return currentTitle;
2487
+ }
2488
+ if (normalizedParentTitle.length > 0 && nextTitle === normalizedParentTitle) {
2489
+ return currentTitle;
2490
+ }
2491
+ return nextTitle;
2492
+ }
2493
+ if (nextTitle.length === 0) {
2494
+ return currentTitle;
2495
+ }
2496
+ if (provider === "codex" && isSyntheticCodexSessionTitle(nextTitle)) {
2497
+ return currentTitle;
2498
+ }
2499
+ if (normalizedParentTitle.length > 0 && nextTitle === normalizedParentTitle && currentTitle !== normalizedParentTitle) {
2500
+ return currentTitle;
2501
+ }
2502
+ return nextTitle;
2503
+ }
2504
+ function isSyntheticCodexSessionTitle(title) {
2505
+ return (/^rollout-\d{4}-\d{2}-\d{2}t/i.test(title) ||
2506
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(title));
2507
+ }
1994
2508
  function shouldRemoveHiddenClaudeDebugSession(session) {
1995
2509
  const normalizedRawStoreRef = session.rawStoreRef.replaceAll("\\", "/");
1996
2510
  if (normalizedRawStoreRef.includes("/subagents/")) {
@@ -1999,11 +2513,34 @@ function shouldRemoveHiddenClaudeDebugSession(session) {
1999
2513
  return (/^agent-[^/]+$/i.test(session.providerSessionId) &&
2000
2514
  /\/agent-[^/]+\.jsonl$/i.test(normalizedRawStoreRef));
2001
2515
  }
2516
+ const STALE_RUNTIME_WITHOUT_INSPECTION_GRACE_MS = 120_000;
2517
+ function shouldClearStaleRuntimeWithoutInspection(current, inspection, nowMs) {
2518
+ if (!current || current.activitySource !== "runtime") {
2519
+ return false;
2520
+ }
2521
+ if (current.runningState !== "starting" && current.runningState !== "running") {
2522
+ return false;
2523
+ }
2524
+ if (inspection.lastEventAt || inspection.completedAtCandidate || inspection.errorCode) {
2525
+ return false;
2526
+ }
2527
+ if (!current.lastEventAt) {
2528
+ return true;
2529
+ }
2530
+ const lastEventAtMs = Date.parse(current.lastEventAt);
2531
+ if (!Number.isFinite(lastEventAtMs)) {
2532
+ return true;
2533
+ }
2534
+ return nowMs - lastEventAtMs > STALE_RUNTIME_WITHOUT_INSPECTION_GRACE_MS;
2535
+ }
2002
2536
  function shouldPreserveRuntimeTerminalState(current, inspection) {
2003
2537
  if (!current || current.activitySource !== "runtime") {
2004
2538
  return false;
2005
2539
  }
2006
- if (!inspection.lastEventAt || !current.lastEventAt) {
2540
+ if (!inspection.lastEventAt) {
2541
+ return !shouldClearStaleRuntimeWithoutInspection(current, inspection, Date.now());
2542
+ }
2543
+ if (!current.lastEventAt) {
2007
2544
  return true;
2008
2545
  }
2009
2546
  if (isTerminalRunningState(current.runningState)) {
@@ -2056,4 +2593,31 @@ function resolveActivityState(runningState, completedAt, lastSeenAt) {
2056
2593
  }
2057
2594
  return "idle";
2058
2595
  }
2596
+ function buildReconstructedForkPrompt(input) {
2597
+ const lines = [
2598
+ input.sourceTitle
2599
+ ? `源会话:${input.sourceTitle}`
2600
+ : "源会话:未命名会话",
2601
+ `源 provider:${input.sourceProvider}`,
2602
+ `目标 provider:${input.targetProvider}`,
2603
+ input.sourceType === "message"
2604
+ ? "分叉方式:从指定消息点重建后续上下文"
2605
+ : "分叉方式:从整条会话重建上下文",
2606
+ "",
2607
+ "下面是需要继承到新会话里的历史文本。",
2608
+ "请把这些内容当作已经发生过的上下文事实,不要逐条复述,也不要把它们当成新的用户问题重新回答。",
2609
+ "后续我会在这条新分支里继续追加新的指令。",
2610
+ ""
2611
+ ];
2612
+ if (input.messages.length === 0) {
2613
+ lines.push("当前没有可继承的历史文本。");
2614
+ return lines.join("\n");
2615
+ }
2616
+ for (const message of input.messages) {
2617
+ lines.push(message.role === "user" ? "[用户]" : "[助手]");
2618
+ lines.push(message.content.trim());
2619
+ lines.push("");
2620
+ }
2621
+ return lines.join("\n").trim();
2622
+ }
2059
2623
  //# sourceMappingURL=session-history-service.js.map