evolcore 0.0.35 → 0.0.36
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/CHANGELOG.md +31 -0
- package/README.md +6 -0
- package/bin/install-codex-managed-hooks.mjs +20 -3
- package/dist/agents/baseagent-availability.js +1 -1
- package/dist/agents/baseagent.js +88 -0
- package/dist/agents/claude-runner.js +59 -12
- package/dist/agents/codex/app-server-client.js +18 -1
- package/dist/agents/codex-runner.js +262 -21
- package/dist/agents/ecagent-runner.js +47 -11
- package/dist/agents/gemini-runner.js +36 -12
- package/dist/aun/aid/client.js +4 -3
- package/dist/aun/aid/control-aid.js +270 -8
- package/dist/aun/aid/identity.js +6 -3
- package/dist/aun/aid/ownership-operation.js +58 -0
- package/dist/aun/aid/store.js +1 -0
- package/dist/aun/group/fs-download.js +67 -2
- package/dist/aun/group/history.js +20 -5
- package/dist/aun/group/operations.js +8 -2
- package/dist/aun/menu/client.js +39 -2
- package/dist/aun/menu/transport.js +1 -1
- package/dist/aun/msg/history.js +43 -4
- package/dist/aun/msg/managed-operation.js +30 -53
- package/dist/aun/msg/p2p.js +51 -15
- package/dist/aun/outbox.js +4 -0
- package/dist/channels/aun.js +260 -73
- package/dist/channels/daemon.js +6 -4
- package/dist/channels/feishu.js +3 -0
- package/dist/cli/agent-command.js +161 -223
- package/dist/cli/agent-create.js +190 -223
- package/dist/cli/agent.js +3 -1
- package/dist/cli/aid-bind-command.js +53 -0
- package/dist/cli/aun-commands.js +384 -91
- package/dist/cli/bench.js +1 -1
- package/dist/cli/cli-argv.js +74 -0
- package/dist/cli/daemon-commands.js +105 -74
- package/dist/cli/diag.js +1 -1
- package/dist/cli/fs-command.js +18 -19
- package/dist/cli/getmeta-command.js +159 -6
- package/dist/cli/index.js +6 -12
- package/dist/cli/init-channel.js +8 -55
- package/dist/cli/init.js +125 -218
- package/dist/cli/queue-command.js +11 -8
- package/dist/cli/restart-monitor.js +134 -48
- package/dist/cli/session-command.js +1 -1
- package/dist/cli/stats.js +7 -5
- package/dist/cli/thread-command.js +103 -30
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-msg-diag.js +1 -0
- package/dist/config/access-policy.js +2 -2
- package/dist/config/boot-log.js +14 -11
- package/dist/config/config-field-policy.js +9 -1
- package/dist/config/config-manager.js +176 -90
- package/dist/config/config-operation-service.js +7 -14
- package/dist/config/contact-book-store.js +142 -26
- package/dist/config/contact-book.js +4 -2
- package/dist/config/lifecycle.js +1 -1
- package/dist/config/owner-config.js +96 -0
- package/dist/config/peer-role-resolver.js +12 -18
- package/dist/config/providers-config.js +382 -0
- package/dist/config/providers-import.js +74 -0
- package/dist/config/providers-management.js +328 -0
- package/dist/config/providers.js +22 -0
- package/dist/config/resolved-config-op.js +17 -14
- package/dist/config/snapshot.js +28 -16
- package/dist/config-store.js +18 -14
- package/dist/core/agent-ipc-registration.js +21 -63
- package/dist/core/agent-runtime.js +16 -5
- package/dist/core/auth/agent-delegation.js +199 -25
- package/dist/core/auth/auth-gateway.js +44 -2
- package/dist/core/auth/authorization-denial.js +1 -1
- package/dist/core/auth/debug-context.js +2 -1
- package/dist/core/auth/delegation-authorization.js +2 -2
- package/dist/core/auth/delegation-diagnostics.js +4 -1
- package/dist/core/auth/operation-authorizer.js +10 -1
- package/dist/core/auth/operation-catalog.js +19 -1
- package/dist/core/bootstrap-service.js +43 -20
- package/dist/core/channel-loader.js +0 -1
- package/dist/core/channel-runtime.js +30 -4
- package/dist/core/command/agent-control.js +196 -37
- package/dist/core/command/cli-intent-parser.js +60 -4
- package/dist/core/command/command-handler.js +212 -60
- package/dist/core/command/connect-menu.js +1 -0
- package/dist/core/command/diag-service.js +5 -4
- package/dist/core/command/file-search.js +368 -0
- package/dist/core/command/menu-catalog.js +2 -1
- package/dist/core/command/menu-handler.js +292 -107
- package/dist/core/command/menu-protocol.js +23 -0
- package/dist/core/command/role-menu.js +7 -19
- package/dist/core/command/skill-slash.js +130 -25
- package/dist/core/command/slash-handler.js +101 -53
- package/dist/core/command/topic-slash.js +16 -5
- package/dist/core/daemon-bootstrap.js +7 -1
- package/dist/core/daemon-control.js +19 -1
- package/dist/core/daemon-lifecycle.js +2 -1
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/evolagent-registry.js +13 -2
- package/dist/core/evolagent.js +3 -6
- package/dist/core/handoff/ipc-registration.js +2 -2
- package/dist/core/handoff/runtime.js +40 -6
- package/dist/core/handoff/store.js +1 -0
- package/dist/core/inference/text-inference.js +95 -6
- package/dist/core/ipc-registration.js +43 -24
- package/dist/core/memory/consolidation-context.js +4 -1
- package/dist/core/memory/consolidation.js +25 -14
- package/dist/core/message/aun-ipc-sender.js +296 -88
- package/dist/core/message/aun-request-registry.js +226 -0
- package/dist/core/message/create-status.js +33 -4
- package/dist/core/message/message-bridge.js +62 -13
- package/dist/core/message/message-log.js +0 -1
- package/dist/core/message/message-queue.js +4 -1
- package/dist/core/message/message-utils.js +8 -1
- package/dist/core/message/peer-mode.js +17 -9
- package/dist/core/message/response-engine.js +266 -124
- package/dist/core/message/retry-scheduler.js +11 -8
- package/dist/core/model/catalog-service.js +60 -18
- package/dist/core/model/model-catalog.js +112 -9
- package/dist/core/model/model-fallback.js +94 -26
- package/dist/core/ownership/owner-resolver.js +687 -0
- package/dist/core/permission/approval-gateway.js +196 -62
- package/dist/core/permission/ec-command-parser.js +138 -25
- package/dist/core/permission/filesystem-approval-scope.js +141 -0
- package/dist/core/permission/protected-paths.js +14 -1
- package/dist/core/permission/sandbox-runtime.js +171 -18
- package/dist/core/permission/tool-policy.js +13 -2
- package/dist/core/role/runtime-policy.js +3 -4
- package/dist/core/session/session-idle-monitor.js +10 -8
- package/dist/core/session/session-manager.js +72 -45
- package/dist/core/session/session-mapper.js +18 -7
- package/dist/core/session/session-renew.js +5 -4
- package/dist/core/session/session-title.js +2 -1
- package/dist/core/session/session-turn-coordinator.js +4 -2
- package/dist/core/skill/index-generator.js +2 -16
- package/dist/core/task/task-recovery-service.js +15 -2
- package/dist/core/thread/command-parser.js +28 -13
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +11 -8
- package/dist/core/thread/thread-operation.js +79 -44
- package/dist/core/thread/thread-route.js +54 -6
- package/dist/core/thread/thread-service.js +236 -45
- package/dist/core/thread/topic-id.js +248 -0
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/eck/message-renderer.js +12 -2
- package/dist/index.js +50 -27
- package/dist/ipc.js +59 -6
- package/dist/paths.js +1 -0
- package/dist/product.js +1 -0
- package/dist/response-system/engines/v1/proactive-flow.js +10 -3
- package/dist/stats/db.js +8 -0
- package/dist/stats/writer.js +26 -4
- package/dist/trigger/authorization.js +48 -3
- package/dist/trigger/feedback.js +28 -10
- package/dist/trigger/ipc-service.js +86 -10
- package/dist/trigger/local-files.js +55 -23
- package/dist/trigger/scheduler.js +57 -11
- package/dist/trigger/system-definitions.js +1 -0
- package/dist/trigger/validation.js +44 -15
- package/dist/utils/aid-bind.js +17 -297
- package/dist/utils/avatar-upload.js +16 -4
- package/dist/utils/cross-platform.js +109 -34
- package/dist/utils/ecweb-supervisor.js +1 -1
- package/dist/utils/ecweb-utils.js +8 -1
- package/dist/utils/error-utils.js +44 -6
- package/dist/utils/instance-registry.js +12 -2
- package/dist/utils/npm-ops.js +102 -27
- package/dist/utils/package-version-info.js +72 -27
- package/dist/utils/restart-safety.js +50 -0
- package/dist/utils/upgrade-jobs.js +304 -0
- package/dist/utils/windows-autostart.js +17 -10
- package/kits/docs/INDEX.md +5 -0
- package/kits/docs/aun/ABILITY_CALLING.md +239 -0
- package/kits/docs/evolcore/INDEX.md +14 -3
- package/kits/docs/evolcore/agent-create.md +55 -141
- package/kits/docs/evolcore/agent.md +38 -16
- package/kits/docs/evolcore/aid.md +34 -0
- package/kits/docs/evolcore/config.md +20 -1
- package/kits/docs/evolcore/event.md +2 -2
- package/kits/docs/evolcore/fs.md +18 -2
- package/kits/docs/evolcore/group-collaboration.md +13 -246
- package/kits/docs/evolcore/group.md +2 -1
- package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
- package/kits/docs/evolcore/menu-protocol.md +65 -1
- package/kits/docs/evolcore/msg.md +64 -2
- package/kits/docs/evolcore/skill.md +86 -0
- package/kits/docs/evolcore/stats.md +6 -0
- package/kits/docs/evolcore/task.md +2 -0
- package/kits/docs/evolcore/thread-chain.md +145 -51
- package/kits/docs/evolcore/trigger.md +48 -3
- package/kits/eck_manifest.json +21 -0
- package/kits/eck_message_manifest.json +4 -4
- package/kits/migrations/migrate-role-config-v5.mjs +20 -7
- package/kits/rules/01-overview.md +2 -1
- package/kits/rules/03-identity.md +3 -1
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/schemas/_meta.json +16 -1
- package/kits/schemas/agent-config.schema.13.json +44 -2
- package/kits/schemas/contact-book.schema.3.json +5 -0
- package/kits/schemas/daemon.schema.6.json +3 -1
- package/kits/schemas/daemon.schema.8.json +329 -0
- package/kits/schemas/defaults.schema.5.json +39 -3
- package/kits/schemas/providers.schema.1.json +76 -0
- package/kits/schemas/relation-config.schema.9.json +5 -1
- package/kits/skills/.switches.json +28 -0
- package/kits/skills/aid-ability/SKILL.md +214 -0
- package/kits/skills/ec-agent-create/SKILL.md +162 -0
- package/kits/skills/ec-group/SKILL.md +2 -1
- package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
- package/kits/skills/ec-identity/SKILL.md +143 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
- package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
- package/kits/skills/ec-trigger/SKILL.md +167 -0
- package/kits/skills/ec-trigger/references/examples.md +153 -0
- package/kits/skills/thread-ops/SKILL.md +108 -420
- package/kits/templates/message-fragments/thread-context.md +1 -2
- package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
- package/kits/templates/roles/member.json +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +1 -0
- package/kits/templates/system-fragments/channel.md +2 -1
- package/kits/templates/system-fragments/commands.md +5 -4
- package/kits/templates/system-fragments/identity.md +2 -0
- package/kits/templates/system-fragments/session.md +1 -1
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -3
|
@@ -8,6 +8,7 @@ import { isInternalInjectedUserMessage, isToolResultMessage } from './session-tr
|
|
|
8
8
|
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, compactJsonlTail, SESSION_META_COMPACT_POLICY, } from './session-fs-store.js';
|
|
9
9
|
import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, normalizeTopicLabel, normalizeTopicStatus, } from './session-mapper.js';
|
|
10
10
|
import { tryParseChannelKey } from '../channel-loader.js';
|
|
11
|
+
import { isMainSession, isMainSessionThreadId, isTopicSession, isTopicThreadId, mainTopicPeer, safeFormatMainTopicId, sessionLocalTopicId, sessionTopicThreadId, } from '../thread/topic-id.js';
|
|
11
12
|
import { isSystemControlChannel } from '../system-channels.js';
|
|
12
13
|
import path from 'path';
|
|
13
14
|
import fs from 'fs';
|
|
@@ -507,11 +508,12 @@ export class SessionManager {
|
|
|
507
508
|
/** 由 session 自身的 channelType/channelId/selfId/threadId 直接定位其 .jsonl 路径(不扫描目录)。 */
|
|
508
509
|
metaPathForSession(session) {
|
|
509
510
|
const dir = this.ensureChatDirForSession(session);
|
|
510
|
-
|
|
511
|
+
// 主话题落在 chat 根目录;只有话题进 `_threads/`。
|
|
512
|
+
const targetDir = isTopicSession(session) ? path.join(dir, '_threads') : dir;
|
|
511
513
|
return this.metaFilePath(targetDir, session.id);
|
|
512
514
|
}
|
|
513
515
|
forkHistoryPath(session) {
|
|
514
|
-
if (!session
|
|
516
|
+
if (!isTopicSession(session))
|
|
515
517
|
throw new Error('fork history requires a topic session');
|
|
516
518
|
return path.join(this.resolveChatDirFromSession(session), '_threads', `${session.id}.fork-history.json`);
|
|
517
519
|
}
|
|
@@ -574,7 +576,7 @@ export class SessionManager {
|
|
|
574
576
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
575
577
|
const session = loaded?.current;
|
|
576
578
|
const source = session?.metadata?.forkSource;
|
|
577
|
-
if (!session
|
|
579
|
+
if (!isTopicSession(session) || !source)
|
|
578
580
|
throw new Error('fork topic is missing');
|
|
579
581
|
if (source.historyVersion) {
|
|
580
582
|
this.readForkHistory(session);
|
|
@@ -626,7 +628,7 @@ export class SessionManager {
|
|
|
626
628
|
appendMeta(channel, channelId, session) {
|
|
627
629
|
const file = sessionToFile(session);
|
|
628
630
|
const metaPath = this.metaPathForSession(session);
|
|
629
|
-
if (session
|
|
631
|
+
if (isTopicSession(session))
|
|
630
632
|
appendJsonlDurable(metaPath, file);
|
|
631
633
|
else
|
|
632
634
|
appendJsonl(metaPath, file);
|
|
@@ -671,7 +673,7 @@ export class SessionManager {
|
|
|
671
673
|
// the newest backend fence unless this call is one of the two explicit
|
|
672
674
|
// boundary writers. This is a last-line guard against a stale object
|
|
673
675
|
// replaying an old agentSessionId into the JSONL history.
|
|
674
|
-
if (session
|
|
676
|
+
if (isTopicSession(session) && !opts?.backendMutation) {
|
|
675
677
|
const latest = this.readMetaLatest(this.metaPathForSession(session));
|
|
676
678
|
const latestId = normalizeBackendSessionId(latest?.agentSessionId);
|
|
677
679
|
if (latest) {
|
|
@@ -714,7 +716,7 @@ export class SessionManager {
|
|
|
714
716
|
}
|
|
715
717
|
if (!opts?.forceWrite) {
|
|
716
718
|
const metaPath = this.metaPathForSession(session);
|
|
717
|
-
const lastMeta = session
|
|
719
|
+
const lastMeta = isTopicSession(session)
|
|
718
720
|
? readLastJsonlLineStrict(metaPath)
|
|
719
721
|
: readLastJsonlLine(metaPath);
|
|
720
722
|
if (lastMeta && this.sessionFilesEqual(lastMeta, sessionToFile(session))) {
|
|
@@ -776,7 +778,7 @@ export class SessionManager {
|
|
|
776
778
|
return undefined;
|
|
777
779
|
// active.json 不能参与 topic backend boundary/CAS;它可能落后于线程 JSONL。
|
|
778
780
|
const active = this.readActive(fromJsonl.channel, fromJsonl.channelId);
|
|
779
|
-
const base = (
|
|
781
|
+
const base = (isMainSession(fromJsonl) && active && active.id === sessionId) ? active : fromJsonl;
|
|
780
782
|
const current = JSON.parse(JSON.stringify(base));
|
|
781
783
|
return { current };
|
|
782
784
|
}
|
|
@@ -789,8 +791,13 @@ export class SessionManager {
|
|
|
789
791
|
logger.error(`[SessionManager] validateSessionFile: no adapter for baseagent=${session.baseagent}, session=${session.id}`);
|
|
790
792
|
return agentSessionId;
|
|
791
793
|
}
|
|
792
|
-
|
|
794
|
+
const status = adapter.checkStatus?.(session.projectPath, agentSessionId, session.selfAID)
|
|
795
|
+
?? (adapter.checkExists(session.projectPath, agentSessionId, session.selfAID) ? 'exists' : 'missing');
|
|
796
|
+
if (status === 'exists' || status === 'unavailable') {
|
|
797
|
+
if (status === 'unavailable')
|
|
798
|
+
logger.info(`[SessionManager] backend unavailable; preserving ${session.baseagent} session binding: ${agentSessionId}`);
|
|
793
799
|
return agentSessionId;
|
|
800
|
+
}
|
|
794
801
|
logger.warn(`Session file not found for ${session.baseagent}: ${agentSessionId}, clearing session ID`);
|
|
795
802
|
session.agentSessionId = undefined;
|
|
796
803
|
this.persistSession(session, 'sync');
|
|
@@ -800,11 +807,15 @@ export class SessionManager {
|
|
|
800
807
|
if (!session.agentSessionId)
|
|
801
808
|
return false;
|
|
802
809
|
const adapter = this.getFileAdapter(session.baseagent);
|
|
803
|
-
|
|
810
|
+
if (!adapter)
|
|
811
|
+
return false;
|
|
812
|
+
const status = adapter.checkStatus?.(session.projectPath, session.agentSessionId, session.selfAID)
|
|
813
|
+
?? (adapter.checkExists(session.projectPath, session.agentSessionId, session.selfAID) ? 'exists' : 'missing');
|
|
814
|
+
return status !== 'missing';
|
|
804
815
|
}
|
|
805
816
|
getActiveChatType(channel, channelId) {
|
|
806
817
|
const active = this.readActive(channel, channelId);
|
|
807
|
-
if (active &&
|
|
818
|
+
if (active && isMainSession(active))
|
|
808
819
|
return active.chatType || 'private';
|
|
809
820
|
return 'private';
|
|
810
821
|
}
|
|
@@ -968,7 +979,7 @@ export class SessionManager {
|
|
|
968
979
|
}
|
|
969
980
|
}
|
|
970
981
|
assertTopicResumeMutationTarget(current, input) {
|
|
971
|
-
if (
|
|
982
|
+
if (sessionTopicThreadId(current) !== input.expectedThreadId) {
|
|
972
983
|
const error = new Error('topic resumeAt threadId mismatch');
|
|
973
984
|
error.code = 'THREAD_MISMATCH';
|
|
974
985
|
throw error;
|
|
@@ -1035,7 +1046,7 @@ export class SessionManager {
|
|
|
1035
1046
|
if (!input.expectedThreadId.trim())
|
|
1036
1047
|
throw new Error('backend rotation requires a non-empty threadId');
|
|
1037
1048
|
return this.mutateLatestTopicSession(input.sessionId, (current, found) => {
|
|
1038
|
-
if (
|
|
1049
|
+
if (sessionTopicThreadId(current) !== input.expectedThreadId) {
|
|
1039
1050
|
const error = new Error('backend rotation threadId mismatch');
|
|
1040
1051
|
error.code = 'THREAD_MISMATCH';
|
|
1041
1052
|
throw error;
|
|
@@ -1084,7 +1095,7 @@ export class SessionManager {
|
|
|
1084
1095
|
throw error;
|
|
1085
1096
|
}
|
|
1086
1097
|
return this.mutateLatestTopicSession(input.sessionId, (current, found) => {
|
|
1087
|
-
if (!current
|
|
1098
|
+
if (!isTopicSession(current))
|
|
1088
1099
|
return 'stale';
|
|
1089
1100
|
const active = current.metadata?.turnState?.active;
|
|
1090
1101
|
if (!active
|
|
@@ -1223,7 +1234,8 @@ export class SessionManager {
|
|
|
1223
1234
|
// 群聊场景下 conversationId 应为群 ID(AUN 群聊 channelId 即 groupId),私聊为 userId。
|
|
1224
1235
|
// 用于 resolveIdentity fallback 正确命中群成员角色表(见 resolvePeerRoleDetail)。
|
|
1225
1236
|
const identityConversationId = chatType === 'group' ? (metadata?.groupId || channelId) : userId;
|
|
1226
|
-
|
|
1237
|
+
// 主话题的 id(`main:<aid>:<aid>`)不是话题会话目录键:它必须落到 chat 根目录。
|
|
1238
|
+
if (isTopicThreadId(threadId)) {
|
|
1227
1239
|
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus);
|
|
1228
1240
|
session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
1229
1241
|
return session;
|
|
@@ -1232,7 +1244,7 @@ export class SessionManager {
|
|
|
1232
1244
|
const exactDir = this.resolveChatDirExact(channel, channelId, channelType, selfAID);
|
|
1233
1245
|
const activeFile = readJsonFile(path.join(exactDir, 'active.json'));
|
|
1234
1246
|
const active = activeFile ? fileToSession(activeFile) : undefined;
|
|
1235
|
-
if (active &&
|
|
1247
|
+
if (active && isMainSession(active)) {
|
|
1236
1248
|
const validSessionId = this.validateSessionFile(active);
|
|
1237
1249
|
const session = { ...active, agentSessionId: validSessionId };
|
|
1238
1250
|
session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
@@ -1297,7 +1309,7 @@ export class SessionManager {
|
|
|
1297
1309
|
const chatDir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
1298
1310
|
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
1299
1311
|
const existing = allSessions
|
|
1300
|
-
.filter(s => s.projectPath === defaultProjectPath &&
|
|
1312
|
+
.filter(s => s.projectPath === defaultProjectPath && isMainSession(s))
|
|
1301
1313
|
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
1302
1314
|
if (existing) {
|
|
1303
1315
|
const validSessionId = this.validateSessionFile(existing);
|
|
@@ -1342,7 +1354,7 @@ export class SessionManager {
|
|
|
1342
1354
|
channelId,
|
|
1343
1355
|
selfAID,
|
|
1344
1356
|
projectPath: defaultProjectPath,
|
|
1345
|
-
|
|
1357
|
+
rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
|
|
1346
1358
|
baseagent: resolvedBaseagent,
|
|
1347
1359
|
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
1348
1360
|
chatType: chatType || 'private',
|
|
@@ -1378,7 +1390,7 @@ export class SessionManager {
|
|
|
1378
1390
|
error.code = 'BASEAGENT_IMMUTABLE';
|
|
1379
1391
|
throw error;
|
|
1380
1392
|
}
|
|
1381
|
-
if (current
|
|
1393
|
+
if (isTopicSession(current) && Object.prototype.hasOwnProperty.call(updates, 'agentSessionId')) {
|
|
1382
1394
|
const error = new Error('topic backend binding requires rotation or turn-lease activation');
|
|
1383
1395
|
error.code = 'TOPIC_BACKEND_BINDING_PROTECTED';
|
|
1384
1396
|
throw error;
|
|
@@ -1390,7 +1402,7 @@ export class SessionManager {
|
|
|
1390
1402
|
if (updates.metadata !== undefined) {
|
|
1391
1403
|
if (Object.hasOwn(updates.metadata, 'sessionLoop'))
|
|
1392
1404
|
throw topicMetadataOwnershipError('sessionLoop');
|
|
1393
|
-
if (current
|
|
1405
|
+
if (isTopicSession(current)) {
|
|
1394
1406
|
for (const key of Object.keys(updates.metadata)) {
|
|
1395
1407
|
if (TOPIC_METADATA_PROTECTED_KEYS.has(key))
|
|
1396
1408
|
throw topicMetadataOwnershipError(key);
|
|
@@ -1421,7 +1433,7 @@ export class SessionManager {
|
|
|
1421
1433
|
for (const key of Object.keys(patch)) {
|
|
1422
1434
|
if (key === 'sessionLoop')
|
|
1423
1435
|
throw topicMetadataOwnershipError(key);
|
|
1424
|
-
if (current
|
|
1436
|
+
if (isTopicSession(current) && TOPIC_METADATA_PROTECTED_KEYS.has(key)) {
|
|
1425
1437
|
throw topicMetadataOwnershipError(key);
|
|
1426
1438
|
}
|
|
1427
1439
|
}
|
|
@@ -1482,7 +1494,7 @@ export class SessionManager {
|
|
|
1482
1494
|
// by this generic lookup; doing so would let the next run create a
|
|
1483
1495
|
// backend without an explicit rotation. Repair/rotation decides how
|
|
1484
1496
|
// to handle an unavailable bound backend.
|
|
1485
|
-
const validSessionId = existing
|
|
1497
|
+
const validSessionId = isTopicSession(existing)
|
|
1486
1498
|
? normalizeBackendSessionId(existing.agentSessionId)
|
|
1487
1499
|
: this.validateSessionFile(existing);
|
|
1488
1500
|
let mutated = false;
|
|
@@ -1537,7 +1549,7 @@ export class SessionManager {
|
|
|
1537
1549
|
channelId,
|
|
1538
1550
|
selfAID: selfAID || '',
|
|
1539
1551
|
projectPath,
|
|
1540
|
-
threadId,
|
|
1552
|
+
rawThreadId: threadId,
|
|
1541
1553
|
baseagent: inheritedBaseagent,
|
|
1542
1554
|
sessionKey,
|
|
1543
1555
|
chatType: effectiveChatType,
|
|
@@ -1632,7 +1644,7 @@ export class SessionManager {
|
|
|
1632
1644
|
const chatDir = this.ensureResolvedChatDir(channel, channelId, identity.channelType, identity.selfAID);
|
|
1633
1645
|
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
1634
1646
|
const target = allSessions
|
|
1635
|
-
.filter(s => s.projectPath === newProjectPath && s.baseagent === baseagent &&
|
|
1647
|
+
.filter(s => s.projectPath === newProjectPath && s.baseagent === baseagent && isMainSession(s))
|
|
1636
1648
|
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
1637
1649
|
if (target) {
|
|
1638
1650
|
const validSessionId = this.validateSessionFile(target);
|
|
@@ -1651,7 +1663,7 @@ export class SessionManager {
|
|
|
1651
1663
|
channelId,
|
|
1652
1664
|
selfAID,
|
|
1653
1665
|
projectPath: newProjectPath,
|
|
1654
|
-
|
|
1666
|
+
rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
|
|
1655
1667
|
baseagent: baseagent,
|
|
1656
1668
|
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
1657
1669
|
chatType: inheritedChatType,
|
|
@@ -1677,7 +1689,7 @@ export class SessionManager {
|
|
|
1677
1689
|
const active = this.readActive(channel, channelId);
|
|
1678
1690
|
if (!active)
|
|
1679
1691
|
return;
|
|
1680
|
-
if (active
|
|
1692
|
+
if (isTopicSession(active)) {
|
|
1681
1693
|
// Topic backend binding is owned exclusively by rotation or an active
|
|
1682
1694
|
// TurnLease activation. Keep this legacy channel-scoped API main-session
|
|
1683
1695
|
// only so stale callers cannot resurrect a renewed topic backend.
|
|
@@ -1692,7 +1704,7 @@ export class SessionManager {
|
|
|
1692
1704
|
if (!loaded)
|
|
1693
1705
|
return 'stale';
|
|
1694
1706
|
const { current } = loaded;
|
|
1695
|
-
if (current
|
|
1707
|
+
if (isTopicSession(current)) {
|
|
1696
1708
|
if (turnLease && normalizeBackendSessionId(agentSessionId)) {
|
|
1697
1709
|
return this.activateTopicBackend({ sessionId, lease: turnLease, agentSessionId });
|
|
1698
1710
|
}
|
|
@@ -1719,7 +1731,7 @@ export class SessionManager {
|
|
|
1719
1731
|
if (!loaded)
|
|
1720
1732
|
return 'missing';
|
|
1721
1733
|
const { current } = loaded;
|
|
1722
|
-
if (current
|
|
1734
|
+
if (isTopicSession(current))
|
|
1723
1735
|
return 'topic';
|
|
1724
1736
|
const currentId = normalizeBackendSessionId(current.agentSessionId);
|
|
1725
1737
|
if (!currentId)
|
|
@@ -1745,7 +1757,7 @@ export class SessionManager {
|
|
|
1745
1757
|
const chatDir = this.ensureResolvedChatDir(channel, channelId, channelType, selfAID);
|
|
1746
1758
|
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
1747
1759
|
const target = allSessions
|
|
1748
|
-
.filter(s => s.projectPath === projectPath && s.baseagent === newBaseagent &&
|
|
1760
|
+
.filter(s => s.projectPath === projectPath && s.baseagent === newBaseagent && isMainSession(s))
|
|
1749
1761
|
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
1750
1762
|
if (target) {
|
|
1751
1763
|
const validSessionId = this.validateSessionFile(target);
|
|
@@ -1760,7 +1772,7 @@ export class SessionManager {
|
|
|
1760
1772
|
channelId,
|
|
1761
1773
|
selfAID,
|
|
1762
1774
|
projectPath,
|
|
1763
|
-
|
|
1775
|
+
rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
|
|
1764
1776
|
baseagent: newBaseagent,
|
|
1765
1777
|
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
1766
1778
|
chatType: inheritedChatType,
|
|
@@ -1834,14 +1846,22 @@ export class SessionManager {
|
|
|
1834
1846
|
hasMainSession(channel, channelId, channelType, selfAID) {
|
|
1835
1847
|
const chatDir = this.resolveChatDirExact(channel, channelId, channelType, selfAID);
|
|
1836
1848
|
const active = readJsonFile(path.join(chatDir, 'active.json'));
|
|
1837
|
-
if (active && !active.threadId)
|
|
1849
|
+
if (active && !isTopicThreadId(active.threadId))
|
|
1838
1850
|
return true;
|
|
1839
1851
|
return scanMetaFiles(chatDir).some(metaFile => {
|
|
1840
1852
|
const session = readLastJsonlLine(path.join(chatDir, metaFile));
|
|
1841
|
-
return !!session && !session.threadId;
|
|
1853
|
+
return !!session && !isTopicThreadId(session.threadId);
|
|
1842
1854
|
});
|
|
1843
1855
|
}
|
|
1844
1856
|
async getThreadSession(channel, channelId, threadId, selfAID) {
|
|
1857
|
+
// 主话题 id 不在 `_threads/index.json` 里,它对应 chat 根目录的 active session。
|
|
1858
|
+
// id 自带双方,必须校验"当前本端确实在两端之内",且另一侧就是 channelId。
|
|
1859
|
+
if (isMainSessionThreadId(threadId)) {
|
|
1860
|
+
const self = selfAID?.replace(/^@/, '') ?? '';
|
|
1861
|
+
if (selfAID && mainTopicPeer(threadId, self) !== channelId.replace(/^@/, ''))
|
|
1862
|
+
return undefined;
|
|
1863
|
+
return this.readActive(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
|
|
1864
|
+
}
|
|
1845
1865
|
let chatDir;
|
|
1846
1866
|
try {
|
|
1847
1867
|
chatDir = this.resolveChatDirSafe(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
|
|
@@ -1873,7 +1893,8 @@ export class SessionManager {
|
|
|
1873
1893
|
.filter(session => {
|
|
1874
1894
|
if (archiveFilter === 'all')
|
|
1875
1895
|
return true;
|
|
1876
|
-
|
|
1896
|
+
// 主话题没有业务状态,永远算"活跃"
|
|
1897
|
+
if (!isTopicSession(session))
|
|
1877
1898
|
return archiveFilter !== 'archived';
|
|
1878
1899
|
const archived = session.topicStatus === 'archived';
|
|
1879
1900
|
return archiveFilter === 'archived' ? archived : !archived;
|
|
@@ -1937,19 +1958,24 @@ export class SessionManager {
|
|
|
1937
1958
|
current.metadata = metadata;
|
|
1938
1959
|
this.persistSession(current, 'sync');
|
|
1939
1960
|
}
|
|
1940
|
-
async renameSession(sessionId, newName) {
|
|
1961
|
+
async renameSession(sessionId, newName, nameUpdatedAt) {
|
|
1941
1962
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1942
1963
|
if (!loaded)
|
|
1943
1964
|
return false;
|
|
1944
1965
|
const { current } = loaded;
|
|
1945
1966
|
current.name = newName;
|
|
1967
|
+
// 主话题的名称版本跟着会话走(话题在索引条目上)。远端同步来的改名必须
|
|
1968
|
+
// 带版本,否则后续无法判断"谁更新"。本地改名不传时保持不写版本。
|
|
1969
|
+
if (nameUpdatedAt !== undefined) {
|
|
1970
|
+
current.metadata = { ...(current.metadata ?? {}), nameUpdatedAt };
|
|
1971
|
+
}
|
|
1946
1972
|
this.persistSession(current, 'sync');
|
|
1947
1973
|
return true;
|
|
1948
1974
|
}
|
|
1949
1975
|
async setTopicLabel(sessionId, label) {
|
|
1950
1976
|
const normalized = normalizeTopicLabel(label);
|
|
1951
1977
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1952
|
-
if (!loaded
|
|
1978
|
+
if (!loaded || !isTopicSession(loaded.current))
|
|
1953
1979
|
return false;
|
|
1954
1980
|
loaded.current.label = normalized;
|
|
1955
1981
|
this.persistSession(loaded.current, 'sync', { topicMutation: true });
|
|
@@ -1958,7 +1984,7 @@ export class SessionManager {
|
|
|
1958
1984
|
async setTopicStatus(sessionId, status) {
|
|
1959
1985
|
const normalized = normalizeTopicStatus(status);
|
|
1960
1986
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1961
|
-
if (!loaded
|
|
1987
|
+
if (!loaded || !isTopicSession(loaded.current))
|
|
1962
1988
|
return false;
|
|
1963
1989
|
loaded.current.topicStatus = normalized;
|
|
1964
1990
|
this.persistSession(loaded.current, 'sync', { topicMutation: true });
|
|
@@ -2115,7 +2141,7 @@ export class SessionManager {
|
|
|
2115
2141
|
channelId,
|
|
2116
2142
|
selfAID,
|
|
2117
2143
|
projectPath,
|
|
2118
|
-
|
|
2144
|
+
rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
|
|
2119
2145
|
baseagent,
|
|
2120
2146
|
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
2121
2147
|
chatType: inheritedChatType,
|
|
@@ -2155,7 +2181,7 @@ export class SessionManager {
|
|
|
2155
2181
|
}
|
|
2156
2182
|
async createForkedSession(sourceSession, forkedAgentSessionId, name) {
|
|
2157
2183
|
const channelType = sourceSession.channelType || sourceSession.channel;
|
|
2158
|
-
const
|
|
2184
|
+
const sourceTopicId = sessionTopicThreadId(sourceSession);
|
|
2159
2185
|
const session = {
|
|
2160
2186
|
id: generateSessionId(),
|
|
2161
2187
|
channel: sourceSession.channel,
|
|
@@ -2163,9 +2189,9 @@ export class SessionManager {
|
|
|
2163
2189
|
channelId: sourceSession.channelId,
|
|
2164
2190
|
selfAID: sourceSession.selfAID,
|
|
2165
2191
|
projectPath: sourceSession.projectPath,
|
|
2166
|
-
|
|
2192
|
+
rawThreadId: sourceTopicId ?? sessionLocalTopicId(sourceSession) ?? '',
|
|
2167
2193
|
baseagent: sourceSession.baseagent,
|
|
2168
|
-
sessionKey: formatSessionKey(channelType, sourceSession.channelId,
|
|
2194
|
+
sessionKey: formatSessionKey(channelType, sourceSession.channelId, sourceTopicId ?? DEFAULT_THREAD_ID),
|
|
2169
2195
|
chatType: sourceSession.chatType || 'private',
|
|
2170
2196
|
agentSessionId: forkedAgentSessionId,
|
|
2171
2197
|
metadata: {},
|
|
@@ -2209,7 +2235,7 @@ export class SessionManager {
|
|
|
2209
2235
|
replyContext: { threadId: targetThreadId },
|
|
2210
2236
|
forkSource: {
|
|
2211
2237
|
sessionId: sourceSession.id,
|
|
2212
|
-
threadId: sourceSession
|
|
2238
|
+
threadId: sessionTopicThreadId(sourceSession) ?? null,
|
|
2213
2239
|
assistantMessageId: options.sourceAssistantMessageId,
|
|
2214
2240
|
...(options.requestedSourceAssistantMessageId
|
|
2215
2241
|
? { requestedAssistantMessageId: options.requestedSourceAssistantMessageId } : {}),
|
|
@@ -2229,7 +2255,7 @@ export class SessionManager {
|
|
|
2229
2255
|
channelId: sourceSession.channelId,
|
|
2230
2256
|
selfAID: sourceSession.selfAID,
|
|
2231
2257
|
projectPath: sourceSession.projectPath,
|
|
2232
|
-
|
|
2258
|
+
rawThreadId: targetThreadId,
|
|
2233
2259
|
baseagent: sourceSession.baseagent,
|
|
2234
2260
|
sessionKey,
|
|
2235
2261
|
chatType: 'private',
|
|
@@ -2277,7 +2303,7 @@ export class SessionManager {
|
|
|
2277
2303
|
projectPath: session.projectPath,
|
|
2278
2304
|
name: session.name,
|
|
2279
2305
|
chatType: session.chatType,
|
|
2280
|
-
threadId: session
|
|
2306
|
+
threadId: sessionTopicThreadId(session),
|
|
2281
2307
|
baseagent: session.baseagent,
|
|
2282
2308
|
timestamp: now,
|
|
2283
2309
|
});
|
|
@@ -2301,7 +2327,8 @@ export class SessionManager {
|
|
|
2301
2327
|
channelId: sourceSession.channelId,
|
|
2302
2328
|
selfAID: sourceSession.selfAID,
|
|
2303
2329
|
projectPath: sourceSession.projectPath,
|
|
2304
|
-
|
|
2330
|
+
// 续期沿用原会话的话题身份:主话题保持规范 id,话题保持原 id。
|
|
2331
|
+
rawThreadId: sessionLocalTopicId(sourceSession) ?? '',
|
|
2305
2332
|
baseagent: context?.baseagent || sourceSession.baseagent,
|
|
2306
2333
|
sessionKey: formatSessionKey(sourceSession.channelType || sourceSession.channel, sourceSession.channelId, DEFAULT_THREAD_ID),
|
|
2307
2334
|
chatType: sourceSession.chatType || 'private',
|
|
@@ -2328,7 +2355,7 @@ export class SessionManager {
|
|
|
2328
2355
|
sourceChannel: sourceSession.channelType || sourceSession.channel,
|
|
2329
2356
|
sourceChannelId: sourceSession.channelId,
|
|
2330
2357
|
sourceChatType: sourceSession.chatType,
|
|
2331
|
-
sourceThreadId: sourceSession
|
|
2358
|
+
sourceThreadId: sessionTopicThreadId(sourceSession),
|
|
2332
2359
|
sourceSelfAid: sourceSession.selfAID,
|
|
2333
2360
|
sourcePeerId: sourceSession.metadata?.peerId,
|
|
2334
2361
|
timestamp: Date.now(),
|
|
@@ -2447,7 +2474,7 @@ export class SessionManager {
|
|
|
2447
2474
|
channelId,
|
|
2448
2475
|
selfAID,
|
|
2449
2476
|
projectPath,
|
|
2450
|
-
|
|
2477
|
+
rawThreadId: safeFormatMainTopicId(selfAID, channelId) ?? '',
|
|
2451
2478
|
baseagent: resolvedBaseagent,
|
|
2452
2479
|
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
2453
2480
|
chatType: inheritedChatType,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatTimestamp } from './session-fs-store.js';
|
|
2
|
+
import { classifyThreadId, sessionThreadKind } from '../thread/topic-id.js';
|
|
2
3
|
/** Default topic component used by the stable session routing key. */
|
|
3
4
|
export const DEFAULT_THREAD_ID = 'main';
|
|
4
5
|
export function normalizeTopicLabel(value) {
|
|
@@ -98,7 +99,10 @@ export function sessionToFile(session) {
|
|
|
98
99
|
}
|
|
99
100
|
const now = session.updatedAt || Date.now();
|
|
100
101
|
const channelType = session.channelType || session.channel;
|
|
101
|
-
|
|
102
|
+
// sessionKey 的第三段始终是渠道级占位 `main`:主话题的规范 id 会随关系变化,
|
|
103
|
+
// 而 sessionKey 是路由键,不能因为对方 AID 不同就换一个存储位置。
|
|
104
|
+
const kind = sessionThreadKind(session);
|
|
105
|
+
const threadId = kind.kind === 'topic' ? kind.threadId : DEFAULT_THREAD_ID;
|
|
102
106
|
if (!session.baseagent) {
|
|
103
107
|
throw new Error(`[session-mapper] sessionToFile: session.baseagent is empty for session ${session.id}`);
|
|
104
108
|
}
|
|
@@ -110,13 +114,16 @@ export function sessionToFile(session) {
|
|
|
110
114
|
sessionKey: formatSessionKey(channelType, session.channelId, threadId),
|
|
111
115
|
selfAID: session.selfAID,
|
|
112
116
|
baseagent: session.baseagent,
|
|
113
|
-
|
|
117
|
+
// 主话题落盘的是规范 id(`main:<aid>:<aid>`),不是空串——展示、持久化、
|
|
118
|
+
// 消费必须是同一个字符串。内部对端(bg:/trigger:/daemon:)与缺 selfAID 的
|
|
119
|
+
// 非 AUN 渠道拼不出 id,退回空串,读取端用 isMainSessionThreadId 统一识别。
|
|
120
|
+
threadId: kind.kind === 'topic' ? kind.threadId : (kind.topicId ?? ''),
|
|
114
121
|
chatType: session.chatType || 'private',
|
|
115
122
|
projectPath: session.projectPath,
|
|
116
123
|
agentSessionId: session.agentSessionId ?? null,
|
|
117
124
|
name: session.name ?? null,
|
|
118
125
|
...(session.label ? { label: session.label } : {}),
|
|
119
|
-
...(
|
|
126
|
+
...(kind.kind === 'topic' ? { topicStatus: session.topicStatus ?? 'pending' } : {}),
|
|
120
127
|
activeTask: session.processingState ?? null,
|
|
121
128
|
metadata,
|
|
122
129
|
createdAt: session.createdAt,
|
|
@@ -153,22 +160,26 @@ export function fileToSession(file) {
|
|
|
153
160
|
if (v !== undefined)
|
|
154
161
|
metadata[k] = v;
|
|
155
162
|
}
|
|
163
|
+
// 主话题的 threadId 在旧文件里是空串(更早是 "main")。读入时统一补成规范 id,
|
|
164
|
+
// 让内存中的 Session 与 list/CLI 看到的是同一个字符串;写回时 sessionToFile 会落盘。
|
|
165
|
+
const kind = classifyThreadId(file.threadId, file.selfAID, file.channelId);
|
|
166
|
+
const isTopic = kind.kind === 'topic';
|
|
156
167
|
return {
|
|
157
168
|
id: file.id,
|
|
158
169
|
channel: file.channel,
|
|
159
170
|
channelType: file.channelType,
|
|
160
171
|
channelId: file.channelId,
|
|
161
|
-
sessionKey: file.sessionKey || formatSessionKey(file.channelType, file.channelId,
|
|
172
|
+
sessionKey: file.sessionKey || formatSessionKey(file.channelType, file.channelId, isTopic ? kind.threadId : DEFAULT_THREAD_ID),
|
|
162
173
|
selfAID: file.selfAID,
|
|
163
174
|
baseagent,
|
|
164
|
-
threadId:
|
|
175
|
+
rawThreadId: isTopic ? kind.threadId : (kind.topicId ?? ''),
|
|
165
176
|
chatType: file.chatType,
|
|
166
177
|
projectPath: file.projectPath,
|
|
167
178
|
agentSessionId: file.agentSessionId ?? undefined,
|
|
168
179
|
name: file.name ?? undefined,
|
|
169
|
-
...(
|
|
180
|
+
...(isTopic && (file.label || (file.lable && file.lable !== '归档'))
|
|
170
181
|
? { label: file.label || file.lable } : {}),
|
|
171
|
-
...(
|
|
182
|
+
...(isTopic ? { topicStatus: file.topicStatus
|
|
172
183
|
? normalizeTopicStatus(file.topicStatus)
|
|
173
184
|
: file.lable === '归档' ? 'archived' : 'pending' } : {}),
|
|
174
185
|
processingState: file.activeTask ?? undefined,
|
|
@@ -6,6 +6,7 @@ import { logger } from '../../utils/logger.js';
|
|
|
6
6
|
import { isTransientProtocolMessage, messageLogPath } from '../message/message-log.js';
|
|
7
7
|
import { readAllJsonlLines } from './session-fs-store.js';
|
|
8
8
|
import { getPackageRoot } from '../../paths.js';
|
|
9
|
+
import { isTopicSession, sessionTopicThreadId } from '../thread/topic-id.js';
|
|
9
10
|
const DEFAULT_AFTER_HOURS = 24;
|
|
10
11
|
const DEFAULT_FALLBACK = 'continue';
|
|
11
12
|
const DEFAULT_NEW_PROBABILITY_THRESHOLD = 0.85;
|
|
@@ -159,7 +160,7 @@ export class SessionRenewService {
|
|
|
159
160
|
return { session: request.session, renewed: false };
|
|
160
161
|
// Thread / processing sessions are already an explicit routing decision.
|
|
161
162
|
// Do not let a cached or active main session replace them before resolveLocked().
|
|
162
|
-
if (request.session
|
|
163
|
+
if (isTopicSession(request.session) || request.session.processingState) {
|
|
163
164
|
return { session: request.session, renewed: false };
|
|
164
165
|
}
|
|
165
166
|
const routeKey = this.routeKey(request);
|
|
@@ -220,11 +221,11 @@ export class SessionRenewService {
|
|
|
220
221
|
let decision;
|
|
221
222
|
let source = 'model';
|
|
222
223
|
let judgeMetadata = {
|
|
223
|
-
model: effectiveConfig.decision_model?.model,
|
|
224
|
+
model: effectiveConfig.Services?.decision ? effectiveConfig.Services.decision.model : effectiveConfig.decision_model?.model,
|
|
224
225
|
threshold,
|
|
225
226
|
};
|
|
226
227
|
try {
|
|
227
|
-
const model = effectiveConfig.decision_model?.model?.trim();
|
|
228
|
+
const model = (effectiveConfig.Services?.decision ? effectiveConfig.Services.decision.model : effectiveConfig.decision_model?.model)?.trim();
|
|
228
229
|
if (!model)
|
|
229
230
|
throw new SessionRenewJudgeError('decision_model_missing');
|
|
230
231
|
const provider = this.inferenceProviderResolver.getDecisionInferenceProvider(request.selfAid);
|
|
@@ -355,7 +356,7 @@ export class SessionRenewService {
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
routeKey(request) {
|
|
358
|
-
return [request.channelType, request.selfAid || '', request.channelId, request.session
|
|
359
|
+
return [request.channelType, request.selfAid || '', request.channelId, sessionTopicThreadId(request.session) ?? ''].join('#');
|
|
359
360
|
}
|
|
360
361
|
async withLock(key, operation) {
|
|
361
362
|
const previous = this.locks.get(key) ?? Promise.resolve();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTopicThreadId } from '../thread/topic-id.js';
|
|
1
2
|
const SYSTEM_PROMPT_MARKERS = [
|
|
2
3
|
'--- [SYSTEM_PROMPT_END] ---',
|
|
3
4
|
'<system-reminder>',
|
|
@@ -150,5 +151,5 @@ export function shouldAutoFillSessionTitle(title, threadId) {
|
|
|
150
151
|
const normalized = sanitizeSessionTitle(title);
|
|
151
152
|
if (normalized === '默认会话')
|
|
152
153
|
return true;
|
|
153
|
-
return
|
|
154
|
+
return isTopicThreadId(threadId) && normalized === '话题会话';
|
|
154
155
|
}
|
|
@@ -107,7 +107,7 @@ export class SessionTurnCoordinator {
|
|
|
107
107
|
await this.persist(session, next);
|
|
108
108
|
return cloneTurnMetadata(next);
|
|
109
109
|
}
|
|
110
|
-
async completeInterrupt(session, reason) {
|
|
110
|
+
async completeInterrupt(session, reason, recoverable) {
|
|
111
111
|
const state = this.hydrate(session);
|
|
112
112
|
const active = state.active;
|
|
113
113
|
if (!active) {
|
|
@@ -139,6 +139,7 @@ export class SessionTurnCoordinator {
|
|
|
139
139
|
? 'cancelled'
|
|
140
140
|
: 'superseded',
|
|
141
141
|
reason,
|
|
142
|
+
...(recoverable !== undefined ? { recoverable } : {}),
|
|
142
143
|
finishedAt: Date.now(),
|
|
143
144
|
},
|
|
144
145
|
};
|
|
@@ -213,7 +214,7 @@ export class SessionTurnCoordinator {
|
|
|
213
214
|
await this.persist(session, next);
|
|
214
215
|
return decision;
|
|
215
216
|
}
|
|
216
|
-
async finish(session, lease, status, reason) {
|
|
217
|
+
async finish(session, lease, status, reason, recoverable) {
|
|
217
218
|
const state = this.hydrate(session);
|
|
218
219
|
if (!this.isCurrent(lease))
|
|
219
220
|
return false;
|
|
@@ -227,6 +228,7 @@ export class SessionTurnCoordinator {
|
|
|
227
228
|
status,
|
|
228
229
|
recoveryContext: terminalRecoveryContext(session, state.active?.recoveryContext),
|
|
229
230
|
reason,
|
|
231
|
+
...(recoverable !== undefined ? { recoverable } : {}),
|
|
230
232
|
finishedAt: Date.now(),
|
|
231
233
|
},
|
|
232
234
|
};
|
|
@@ -35,25 +35,11 @@ function generateOneIndex(indexDir, filename, skills, label) {
|
|
|
35
35
|
}
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
// 根目录从任一条目反推(同组内同源,父目录一致)
|
|
39
|
-
const rootDir = path.dirname(skills[0].skillDir);
|
|
40
|
-
// 示例用第一个技能的真实路径——模型照抄真路径比按规则推导可靠
|
|
41
|
-
const exampleName = skills[0].name;
|
|
42
|
-
const examplePath = path.join(skills[0].skillDir, 'SKILL.md');
|
|
43
|
-
// 路径规则块(用 path.sep,示例路径用真实技能名)
|
|
44
|
-
const sep = path.sep;
|
|
45
38
|
const header = `[skills-${label}] 本 Agent 已启用 ${skills.length} 个${label === 'builtin' ? '内置' : '本机'}技能。
|
|
46
39
|
|
|
47
|
-
加载方式:
|
|
40
|
+
加载方式:
|
|
41
|
+
EC 技能用 \`ec skill load <技能名>\` 加载,命令直接输出 SKILL.md 正文并附带技能目录的绝对路径。
|
|
48
42
|
它们不是宿主工具自带的技能或斜杠命令,用 Skill/斜杠命令加载会报 Unknown skill/未知命令。
|
|
49
|
-
|
|
50
|
-
路径规则:
|
|
51
|
-
根目录: ${rootDir}
|
|
52
|
-
技能目录 = 根目录 + "${sep}" + 技能名
|
|
53
|
-
SKILL.md = 技能目录 + "${sep}SKILL.md"
|
|
54
|
-
例: 技能名为 ${exampleName},其 SKILL.md 完整路径为
|
|
55
|
-
${examplePath}
|
|
56
|
-
执行脚本时必须写完整绝对路径。
|
|
57
43
|
`;
|
|
58
44
|
// 技能条目(name + description)
|
|
59
45
|
const items = [];
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { replyContextFromSession } from '../message/message-utils.js';
|
|
2
|
+
import { sessionTopicThreadId } from '../thread/topic-id.js';
|
|
2
3
|
export const TASK_RECOVERY_PROMPT = '继续当前会话尚未完成的任务。先核对最新用户指令、已有结果和任务状态;若已完成、用户已停止或正在等待必要输入/审批,则不要重复执行。对可能已产生副作用的步骤先核实结果,再继续未完成的工作。';
|
|
4
|
+
/**
|
|
5
|
+
* Turn failure reason prefix: the agent run finished normally but delivering
|
|
6
|
+
* its final reply failed. Re-running the agent cannot fix a transport failure,
|
|
7
|
+
* so automatic recovery must not pick these turns up.
|
|
8
|
+
*/
|
|
9
|
+
export const REPLY_DELIVERY_FAILED_REASON = 'reply_delivery_failed';
|
|
3
10
|
function lastTask(state) {
|
|
4
11
|
return state?.active ?? state?.lastTerminal;
|
|
5
12
|
}
|
|
@@ -22,7 +29,11 @@ function canRecover(session, automatic, origin) {
|
|
|
22
29
|
|| (context?.agentSessionId && context.agentSessionId !== session.agentSessionId))
|
|
23
30
|
return false;
|
|
24
31
|
const reason = 'cancelReason' in task ? task.cancelReason : 'reason' in task ? task.reason : undefined;
|
|
25
|
-
|
|
32
|
+
// Provider/model configuration is deterministic: an automatic retry resumes
|
|
33
|
+
// the same failed thread on the same missing model and only extends the loop.
|
|
34
|
+
if (reason && /interaction|approval|permission|denied|recalled|new_message|empty_prompt|invalid_model|model_not_found|model_unavailable/i.test(reason))
|
|
35
|
+
return false;
|
|
36
|
+
if (automatic && reason?.startsWith(REPLY_DELIVERY_FAILED_REASON))
|
|
26
37
|
return false;
|
|
27
38
|
if (task.status === 'cancelled' || task.status === 'superseded' || reason === 'stop' || reason === 'explicit_cancel') {
|
|
28
39
|
return !automatic && (reason === 'stop' || reason === 'explicit_cancel');
|
|
@@ -31,6 +42,8 @@ function canRecover(session, automatic, origin) {
|
|
|
31
42
|
return true;
|
|
32
43
|
if (state?.active)
|
|
33
44
|
return false;
|
|
45
|
+
if (state?.lastTerminal?.recoverable !== true)
|
|
46
|
+
return false;
|
|
34
47
|
return !context?.originTriggerId || sameRecoveryRun(context.recovery, origin);
|
|
35
48
|
}
|
|
36
49
|
/** Check again at dequeue: a newer task, /stop, /renew or backend switch wins. */
|
|
@@ -150,7 +163,7 @@ export class TaskRecoveryService {
|
|
|
150
163
|
} } : {}),
|
|
151
164
|
channel: session.metadata?.channelKey || session.channel,
|
|
152
165
|
channelType, channelId: session.channelId, selfAID: session.selfAID,
|
|
153
|
-
baseagent: session.baseagent, threadId: session
|
|
166
|
+
baseagent: session.baseagent, threadId: sessionTopicThreadId(session) ?? '',
|
|
154
167
|
chatType: session.chatType === 'group' ? 'group' : 'private',
|
|
155
168
|
groupId: session.chatType === 'group' ? session.metadata?.groupId || session.channelId : undefined,
|
|
156
169
|
peerId, peerName, peerType,
|