@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.
- package/dist/public/assets/{TerminalPage-4p6EBqrR.js → TerminalPage-BlbQuWi1.js} +1 -1
- package/dist/public/assets/index-1VIm8lVL.css +1 -0
- package/dist/public/assets/index-Dti93O2S.js +109 -0
- package/dist/public/index.html +2 -2
- package/dist/server/config/env.d.ts +1 -0
- package/dist/server/config/env.js +5 -1
- package/dist/server/config/env.js.map +1 -1
- package/dist/server/modules/file/file-controller.d.ts +12 -1
- package/dist/server/modules/file/file-controller.js +72 -1
- package/dist/server/modules/file/file-controller.js.map +1 -1
- package/dist/server/modules/file/file-preview-link-service.d.ts +22 -0
- package/dist/server/modules/file/file-preview-link-service.js +160 -0
- package/dist/server/modules/file/file-preview-link-service.js.map +1 -0
- package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +2 -1
- package/dist/server/modules/sessions/codex-app-server-helper-client.js +103 -0
- package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
- package/dist/server/modules/sessions/codex-app-server-helper-process.js +106 -1
- package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
- package/dist/server/modules/sessions/session-controller.d.ts +21 -0
- package/dist/server/modules/sessions/session-controller.js +23 -1
- package/dist/server/modules/sessions/session-controller.js.map +1 -1
- package/dist/server/modules/sessions/session-history-service.d.ts +34 -2
- package/dist/server/modules/sessions/session-history-service.js +591 -27
- package/dist/server/modules/sessions/session-history-service.js.map +1 -1
- package/dist/server/modules/sessions/session-live-runtime-service.d.ts +5 -0
- package/dist/server/modules/sessions/session-live-runtime-service.js +59 -2
- package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
- package/dist/server/modules/sessions/session-provider-error-mapper.js +66 -0
- package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
- package/dist/server/routes/files.js +2 -0
- package/dist/server/routes/files.js.map +1 -1
- package/dist/server/routes/sessions.js +1 -0
- package/dist/server/routes/sessions.js.map +1 -1
- package/dist/server/server/create-server.d.ts +4 -0
- package/dist/server/server/create-server.js +7 -2
- package/dist/server/server/create-server.js.map +1 -1
- package/dist/server/shared/utils/command-availability.d.ts +1 -0
- package/dist/server/shared/utils/command-availability.js +83 -0
- package/dist/server/shared/utils/command-availability.js.map +1 -0
- package/dist/server/storage/repositories/session-fork-repository.d.ts +8 -0
- package/dist/server/storage/repositories/session-fork-repository.js +69 -0
- package/dist/server/storage/repositories/session-fork-repository.js.map +1 -0
- package/dist/server/storage/repositories/session-index-repository.js +40 -2
- package/dist/server/storage/repositories/session-index-repository.js.map +1 -1
- package/dist/server/storage/sqlite/client.js +107 -0
- package/dist/server/storage/sqlite/client.js.map +1 -1
- package/dist/server/storage/sqlite/schema.sql +30 -0
- package/dist/server/types/domain.d.ts +25 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +6 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +228 -7
- package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +26 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +495 -2
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +94 -5
- package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +5 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +269 -3
- package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +117 -17
- package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +10 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +128 -8
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/services.d.ts +2 -1
- package/node_modules/@codingns/session-sync-core/dist/services.js +55 -8
- package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/types.d.ts +27 -0
- package/package.json +1 -1
- package/dist/public/assets/index-CxeghocY.css +0 -1
- 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({
|
|
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
|
|
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
|
-
|
|
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:
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
|
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(
|
|
1121
|
+
await this.refreshRecentSessionStates(refreshCandidates, userId);
|
|
765
1122
|
}
|
|
766
1123
|
else {
|
|
767
|
-
this.scheduleWorkspaceStateRefresh(workspaceId, userId,
|
|
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
|
-
|
|
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
|
-
:
|
|
1270
|
+
: this.resolvePersistedParentSessionId(sessionId);
|
|
913
1271
|
relationMap.set(sessionId, {
|
|
914
1272
|
parentSessionId,
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|