evolcore 0.0.32 → 0.0.34
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 +61 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +135 -37
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +111 -15
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +112 -54
- package/dist/agents/codex-runner.js +241 -63
- package/dist/agents/ecagent-runner.js +78 -33
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/group/operations.js +0 -1
- package/dist/aun/msg/history.js +1 -0
- package/dist/aun/msg/p2p.js +0 -1
- package/dist/aun/outbox.js +50 -1
- package/dist/channels/aun.js +44 -8
- package/dist/channels/daemon.js +261 -3
- package/dist/cli/agent-command.js +58 -24
- package/dist/cli/agent.js +1 -1
- package/dist/cli/aun-commands.js +368 -31
- package/dist/cli/contact.js +8 -2
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +197 -27
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +98 -67
- package/dist/cli/init.js +2 -2
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +166 -146
- package/dist/cli/queue-command.js +9 -4
- package/dist/cli/session-command.js +194 -0
- package/dist/cli/skill-command.js +78 -2
- package/dist/cli/task-context.js +1 -0
- package/dist/cli/thread-command.js +622 -0
- package/dist/cli/trigger-command.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +6 -3
- package/dist/config/config-manager.js +26 -2
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +45 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +133 -36
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +25 -2
- package/dist/core/auth/operation-catalog.js +49 -0
- package/dist/core/background-session/session-cleanup.js +212 -0
- package/dist/core/background-session/types.js +10 -0
- package/dist/core/bootstrap-messages.js +1 -1
- package/dist/core/bootstrap-service.js +6 -3
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +197 -27
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +51 -1
- package/dist/core/command/menu-handler.js +114 -23
- package/dist/core/command/menu-protocol.js +25 -1
- package/dist/core/command/skill-slash.js +322 -0
- package/dist/core/command/slash-gate.js +6 -4
- package/dist/core/command/slash-handler.js +195 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +175 -3
- package/dist/core/memory/batch-meta.js +132 -0
- package/dist/core/memory/consolidation-context.js +83 -29
- package/dist/core/memory/consolidation-ops-records.js +49 -16
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +264 -205
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +30 -15
- package/dist/core/message/aun-ipc-sender.js +161 -6
- package/dist/core/message/im-renderer.js +169 -10
- package/dist/core/message/message-bridge.js +15 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +6 -1
- package/dist/core/message/response-engine.js +929 -110
- package/dist/core/message/subtask-tool-projection.js +140 -0
- package/dist/core/model/catalog-service.js +17 -12
- package/dist/core/permission/ec-command-parser.js +217 -9
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +30 -62
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +129 -14
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +21 -4
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +222 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -0
- package/dist/core/thread/command-parser.js +125 -0
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +95 -0
- package/dist/core/thread/thread-context-storage.js +131 -0
- package/dist/core/thread/thread-operation.js +258 -0
- package/dist/core/thread/thread-route.js +33 -0
- package/dist/core/thread/thread-service.js +770 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +19 -7
- package/dist/ipc.js +140 -50
- package/dist/paths.js +93 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/authorization.js +57 -28
- package/dist/trigger/feedback.js +13 -2
- package/dist/trigger/parser.js +70 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/cross-platform.js +11 -2
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +143 -1
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +40 -28
- package/kits/docs/evolcore/agent.md +21 -6
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +5 -1
- package/kits/docs/evolcore/fs-architecture.md +4 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +4 -6
- package/kits/docs/evolcore/group.md +11 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -15
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +8 -11
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/prompt-loading-architecture.md +1 -1
- package/kits/eck_manifest.json +55 -40
- package/kits/eck_manifest.memory.json +0 -12
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +199 -0
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -0
- package/kits/schemas/relation-config.schema.9.json +18 -3
- package/kits/schemas/role-config.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.2.json +6 -6
- package/kits/schemas/single-session.schema.3.json +6 -6
- package/kits/skills/.switches.json +12 -1
- package/kits/skills/ec-group/SKILL.md +234 -0
- package/kits/skills/ec-skill/SKILL.md +10 -1
- package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
- package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
- package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
- package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
- package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
- package/kits/skills/thread-ops/SKILL.md +492 -0
- package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
- package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
- package/kits/skills/wechat-article-reader/SKILL.md +97 -0
- package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
- package/kits/templates/bootstrap-welcome.md +7 -7
- package/kits/templates/memory-consolidation/forget.md +7 -2
- package/kits/templates/memory-consolidation/generalize.md +9 -4
- package/kits/templates/memory-consolidation/notes.md +11 -11
- package/kits/templates/memory-consolidation/profile.md +8 -3
- package/kits/templates/memory-consolidation/promote.md +8 -3
- package/kits/templates/memory-consolidation/workflow.md +6 -6
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/message-fragments/item.md +2 -1
- package/kits/templates/message-fragments/thread-context.md +6 -0
- package/kits/templates/message-fragments/trigger-session.md +2 -0
- package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
- package/kits/templates/roles/member.json +2 -0
- package/kits/templates/roles/visitor.json +2 -0
- package/kits/templates/system-fragments/bootstrap.md +67 -72
- package/kits/templates/system-fragments/channel.md +3 -6
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/managed-runtime.md +15 -0
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +7 -6
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +6 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
- package/kits/docs/evolcore/group-fs.md +0 -17
- package/kits/docs/venues/aun-group.md +0 -45
- package/kits/docs/venues/group.md +0 -43
|
@@ -1,25 +1,142 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { ConfigTarget, read as readConfig } from '../../config/config-manager.js';
|
|
3
|
+
import { ConfigTarget, mutateConfig, read as readConfig } from '../../config/config-manager.js';
|
|
4
4
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
5
|
+
import { isSystemControlChannel } from '../system-channels.js';
|
|
5
6
|
import { logger } from '../../utils/logger.js';
|
|
6
7
|
import { atomicWriteJson } from './session-fs-store.js';
|
|
7
8
|
import { createRootCausation } from '../causation/context.js';
|
|
8
|
-
export const
|
|
9
|
-
export
|
|
10
|
-
|
|
9
|
+
export const DEFAULT_SESSION_LOOP_SECONDS = 300;
|
|
10
|
+
export const MIN_SESSION_LOOP_SECONDS = 30;
|
|
11
|
+
export const MAX_SESSION_LOOP_SECONDS = 86400;
|
|
12
|
+
export function normalizeSessionLoopConfig(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
14
|
return undefined;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
const raw = value;
|
|
16
|
+
if (Object.keys(raw).some(key => key !== 'enabled' && key !== 'idleSeconds'))
|
|
17
|
+
return undefined;
|
|
18
|
+
if (raw.enabled === false && Object.keys(raw).every(key => key === 'enabled'))
|
|
19
|
+
return { enabled: false };
|
|
20
|
+
if (raw.enabled !== true || !isValidIdleSeconds(raw.idleSeconds))
|
|
21
|
+
return undefined;
|
|
22
|
+
return { enabled: true, idleSeconds: raw.idleSeconds };
|
|
23
|
+
}
|
|
24
|
+
export function validateIdleSeconds(value) {
|
|
25
|
+
return isValidIdleSeconds(value) ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
export function resolveSessionLoopTarget(session) {
|
|
28
|
+
if (!session || session.deletedAt || session.topicStatus === 'archived'
|
|
29
|
+
|| session.metadata?.sessionType || isSystemControlChannel(session.channel)
|
|
30
|
+
|| (session.chatType !== 'private' && session.chatType !== 'group'))
|
|
31
|
+
return undefined;
|
|
32
|
+
const peerId = session.chatType === 'group'
|
|
33
|
+
? session.metadata?.groupId || session.channelId
|
|
34
|
+
: session.channelId;
|
|
35
|
+
if (!session.selfAID || !peerId)
|
|
21
36
|
return undefined;
|
|
37
|
+
const peerKey = formatPeerKey(session.channelType || session.channel, peerId);
|
|
38
|
+
return { level: session.threadId ? 'topic-session' : 'relation-main', session, peerKey };
|
|
39
|
+
}
|
|
40
|
+
export function readSessionLoopConfig(target) {
|
|
41
|
+
let value;
|
|
42
|
+
if (target.level === 'topic-session')
|
|
43
|
+
value = target.session.metadata?.sessionLoop;
|
|
44
|
+
else {
|
|
45
|
+
try {
|
|
46
|
+
const relation = readConfig(ConfigTarget.Relation, {
|
|
47
|
+
self: target.session.selfAID,
|
|
48
|
+
peerKey: target.peerKey,
|
|
49
|
+
}, { cache: true, expand: true });
|
|
50
|
+
value = relation?.sessionLoop;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
logger.warn(`[SessionLoop] Failed to read configuration for ${target.session.id}; disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const config = normalizeSessionLoopConfig(value);
|
|
58
|
+
if (value !== undefined && !config)
|
|
59
|
+
logger.warn(`[SessionLoop] Invalid configuration for ${target.session.id}; disabled`);
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
export function readSessionLoopConfigForSession(session) {
|
|
63
|
+
const target = resolveSessionLoopTarget(session);
|
|
64
|
+
return target ? readSessionLoopConfig(target) : undefined;
|
|
65
|
+
}
|
|
66
|
+
export function updateRelationSessionLoop(session, config) {
|
|
67
|
+
const target = resolveSessionLoopTarget(session);
|
|
68
|
+
if (!target || target.level !== 'relation-main')
|
|
69
|
+
throw sessionLoopError('INVALID_TARGET', 'relation main session is required');
|
|
70
|
+
const selector = { self: session.selfAID, peerKey: target.peerKey };
|
|
71
|
+
const normalized = normalizeSessionLoopConfig(config);
|
|
72
|
+
if (!normalized)
|
|
73
|
+
throw sessionLoopError('INVALID_ARGUMENT', 'invalid sessionLoop configuration');
|
|
74
|
+
mutateConfig(ConfigTarget.Relation, selector, current => {
|
|
75
|
+
current.sessionLoop = normalized;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function resolveCurrentSessionLoopTarget(store, route) {
|
|
79
|
+
let session;
|
|
80
|
+
try {
|
|
81
|
+
session = route.threadId
|
|
82
|
+
? await store.getThreadSession(route.channel, route.channelId, route.threadId)
|
|
83
|
+
: await store.getActiveSession(route.channel, route.channelId);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw sessionLoopError('SESSION_LOOKUP_FAILED', '无法读取当前会话,拒绝回退到其他会话');
|
|
87
|
+
}
|
|
88
|
+
if (!session || session.deletedAt)
|
|
89
|
+
throw sessionLoopError(route.threadId ? 'NOT_FOUND' : 'NO_ACTIVE_SESSION', '当前会话不存在');
|
|
90
|
+
const target = resolveSessionLoopTarget(session);
|
|
91
|
+
if (!target || !route.selfAid || session.selfAID !== route.selfAid
|
|
92
|
+
|| session.channel !== route.channel || session.channelId !== route.channelId
|
|
93
|
+
|| (session.threadId || '') !== (route.threadId || '')) {
|
|
94
|
+
throw sessionLoopError('INVALID_CONTEXT', '当前会话路由或归属不匹配');
|
|
95
|
+
}
|
|
96
|
+
return target;
|
|
97
|
+
}
|
|
98
|
+
export async function refreshSessionLoopTarget(store, route, expected) {
|
|
99
|
+
const current = await resolveCurrentSessionLoopTarget(store, route);
|
|
100
|
+
if (current.session.id !== expected.session.id || current.peerKey !== expected.peerKey || current.level !== expected.level) {
|
|
101
|
+
throw sessionLoopError('CONFLICT', '当前会话已发生变化,请重新查询');
|
|
22
102
|
}
|
|
103
|
+
return current;
|
|
104
|
+
}
|
|
105
|
+
export function writeSessionLoopConfig(store, target, config) {
|
|
106
|
+
if (target.level === 'topic-session') {
|
|
107
|
+
const session = store.updateSessionLoop(target.session.id, config);
|
|
108
|
+
return { ...target, session };
|
|
109
|
+
}
|
|
110
|
+
const active = store.getActiveSessionSync(target.session.channel, target.session.channelId);
|
|
111
|
+
if (!active || active.deletedAt || active.id !== target.session.id || active.threadId) {
|
|
112
|
+
throw sessionLoopError('CONFLICT', '当前主会话已发生变化,请重新查询');
|
|
113
|
+
}
|
|
114
|
+
updateRelationSessionLoop(active, config);
|
|
115
|
+
return { ...target, session: active };
|
|
116
|
+
}
|
|
117
|
+
export function sessionLoopPayload(target) {
|
|
118
|
+
const config = readSessionLoopConfig(target);
|
|
119
|
+
return {
|
|
120
|
+
target: { level: target.level, sessionId: target.session.id },
|
|
121
|
+
sessionId: target.session.id,
|
|
122
|
+
value: config?.enabled === true,
|
|
123
|
+
...(config?.enabled ? { idleSeconds: config.idleSeconds } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function sessionLoopError(code, message) {
|
|
127
|
+
return Object.assign(new Error(message), { code });
|
|
128
|
+
}
|
|
129
|
+
function isValidIdleSeconds(value) {
|
|
130
|
+
return typeof value === 'number'
|
|
131
|
+
&& Number.isFinite(value)
|
|
132
|
+
&& Number.isSafeInteger(value)
|
|
133
|
+
&& value >= MIN_SESSION_LOOP_SECONDS
|
|
134
|
+
&& value <= MAX_SESSION_LOOP_SECONDS;
|
|
135
|
+
}
|
|
136
|
+
export const MIN_SESSION_IDLE_SECONDS = MIN_SESSION_LOOP_SECONDS;
|
|
137
|
+
export function readSessionLoopIdleSeconds(session) {
|
|
138
|
+
const config = readSessionLoopConfigForSession(session);
|
|
139
|
+
return config?.enabled ? config.idleSeconds : undefined;
|
|
23
140
|
}
|
|
24
141
|
/**
|
|
25
142
|
* Publishes one session:idle event after a runner task ends, using the
|
|
@@ -71,7 +188,17 @@ export class SessionIdleMonitor {
|
|
|
71
188
|
if (event.type !== 'session:switched')
|
|
72
189
|
return;
|
|
73
190
|
this.clearSession(event.fromSessionId);
|
|
74
|
-
this.
|
|
191
|
+
void this.activateMainSession(event.toSessionId);
|
|
192
|
+
};
|
|
193
|
+
onSessionCreated = (event) => {
|
|
194
|
+
if (event.type !== 'session:created' || event.threadId)
|
|
195
|
+
return;
|
|
196
|
+
for (const sessionId of this.sessions.keys())
|
|
197
|
+
void this.pruneInactiveMainSession(sessionId);
|
|
198
|
+
};
|
|
199
|
+
onSessionLoopChanged = (event) => {
|
|
200
|
+
if (event.type === 'session:loop-changed')
|
|
201
|
+
void this.refreshSession(event.sessionId, event.timestamp);
|
|
75
202
|
};
|
|
76
203
|
constructor(options) {
|
|
77
204
|
this.options = options;
|
|
@@ -88,6 +215,8 @@ export class SessionIdleMonitor {
|
|
|
88
215
|
this.options.eventBus.subscribe('task:started', this.onTaskStarted);
|
|
89
216
|
this.options.eventBus.subscribe('session:deleted', this.onSessionDeleted);
|
|
90
217
|
this.options.eventBus.subscribe('session:switched', this.onSessionSwitched);
|
|
218
|
+
this.options.eventBus.subscribe('session:created', this.onSessionCreated);
|
|
219
|
+
this.options.eventBus.subscribe('session:loop-changed', this.onSessionLoopChanged);
|
|
91
220
|
}
|
|
92
221
|
/** Arm restored timers only after event-trigger subscriptions are registered. */
|
|
93
222
|
activate() {
|
|
@@ -108,6 +237,47 @@ export class SessionIdleMonitor {
|
|
|
108
237
|
this.scheduleSession(sessionId);
|
|
109
238
|
}
|
|
110
239
|
}
|
|
240
|
+
async refreshSession(sessionId, changedAt = this.now()) {
|
|
241
|
+
const state = this.sessions.get(sessionId);
|
|
242
|
+
if (!state || !this.active)
|
|
243
|
+
return;
|
|
244
|
+
const generation = this.generations.get(sessionId) ?? 0;
|
|
245
|
+
try {
|
|
246
|
+
const session = await this.options.getSessionById(sessionId);
|
|
247
|
+
if (!session || !this.isEligibleSession(session))
|
|
248
|
+
return;
|
|
249
|
+
const seconds = await this.readIdleSeconds(session);
|
|
250
|
+
if (this.sessions.get(sessionId) !== state || (this.generations.get(sessionId) ?? 0) !== generation)
|
|
251
|
+
return;
|
|
252
|
+
if (state.idleSeconds === seconds)
|
|
253
|
+
return;
|
|
254
|
+
this.clearTimers(sessionId);
|
|
255
|
+
this.generations.set(sessionId, generation + 1);
|
|
256
|
+
state.idleSeconds = seconds;
|
|
257
|
+
state.armedAt = changedAt;
|
|
258
|
+
state.emitted = false;
|
|
259
|
+
this.persistState();
|
|
260
|
+
if (seconds !== undefined && !await this.options.isSessionBusy(sessionId))
|
|
261
|
+
this.scheduleSession(sessionId);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
this.warn(`Failed to refresh loop configuration for ${sessionId}`, error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async refreshRelation(selfAid, peerKey, changedAt = this.now()) {
|
|
268
|
+
for (const sessionId of this.sessions.keys()) {
|
|
269
|
+
try {
|
|
270
|
+
const session = await this.options.getSessionById(sessionId);
|
|
271
|
+
const target = resolveSessionLoopTarget(session);
|
|
272
|
+
if (target?.level === 'relation-main' && target.peerKey === peerKey && session?.selfAID === selfAid) {
|
|
273
|
+
await this.refreshSession(sessionId, changedAt);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
this.warn(`Failed to refresh relation loop configuration for ${sessionId}`, error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
111
281
|
stop() {
|
|
112
282
|
this.active = false;
|
|
113
283
|
this.clearAllTimers();
|
|
@@ -120,6 +290,8 @@ export class SessionIdleMonitor {
|
|
|
120
290
|
this.options.eventBus.unsubscribe('task:started', this.onTaskStarted);
|
|
121
291
|
this.options.eventBus.unsubscribe('session:deleted', this.onSessionDeleted);
|
|
122
292
|
this.options.eventBus.unsubscribe('session:switched', this.onSessionSwitched);
|
|
293
|
+
this.options.eventBus.unsubscribe('session:created', this.onSessionCreated);
|
|
294
|
+
this.options.eventBus.unsubscribe('session:loop-changed', this.onSessionLoopChanged);
|
|
123
295
|
}
|
|
124
296
|
now() {
|
|
125
297
|
return this.options.now?.() ?? Date.now();
|
|
@@ -172,15 +344,21 @@ export class SessionIdleMonitor {
|
|
|
172
344
|
this.clearSession(sessionId);
|
|
173
345
|
return;
|
|
174
346
|
}
|
|
175
|
-
if (!
|
|
347
|
+
if (!this.isEligibleSession(session)) {
|
|
348
|
+
this.clearTimers(sessionId);
|
|
176
349
|
return;
|
|
350
|
+
}
|
|
177
351
|
const idleSeconds = await this.readIdleSeconds(session);
|
|
178
352
|
if (!this.isCurrentCycle(sessionId, state.lastEndedAt, generation))
|
|
179
353
|
return;
|
|
354
|
+
if (this.timers.has(sessionId))
|
|
355
|
+
return;
|
|
180
356
|
const idleMs = toIdleMs(idleSeconds);
|
|
357
|
+
state.idleSeconds = idleSeconds;
|
|
358
|
+
this.persistState();
|
|
181
359
|
if (idleMs === undefined)
|
|
182
360
|
return;
|
|
183
|
-
const delay = Math.max(0, state.lastEndedAt + idleMs - this.now());
|
|
361
|
+
const delay = Math.max(0, (state.armedAt ?? state.lastEndedAt) + idleMs - this.now());
|
|
184
362
|
const timer = setTimeout(() => {
|
|
185
363
|
this.timers.delete(sessionId);
|
|
186
364
|
void this.emitIfIdle(sessionId, state.lastEndedAt, generation);
|
|
@@ -209,14 +387,14 @@ export class SessionIdleMonitor {
|
|
|
209
387
|
this.clearSession(sessionId);
|
|
210
388
|
return;
|
|
211
389
|
}
|
|
212
|
-
if (!
|
|
390
|
+
if (!this.isEligibleSession(session))
|
|
213
391
|
return;
|
|
214
392
|
const idleMs = toIdleMs(await this.readIdleSeconds(session));
|
|
215
393
|
if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
|
|
216
394
|
return;
|
|
217
395
|
if (idleMs === undefined)
|
|
218
396
|
return;
|
|
219
|
-
const remaining = lastEndedAt + idleMs - this.now();
|
|
397
|
+
const remaining = (this.sessions.get(sessionId)?.armedAt ?? lastEndedAt) + idleMs - this.now();
|
|
220
398
|
if (remaining > 0) {
|
|
221
399
|
this.scheduleSession(sessionId);
|
|
222
400
|
return;
|
|
@@ -232,6 +410,8 @@ export class SessionIdleMonitor {
|
|
|
232
410
|
}
|
|
233
411
|
if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
|
|
234
412
|
return;
|
|
413
|
+
if (!this.isEligibleSession(session))
|
|
414
|
+
return;
|
|
235
415
|
if (busy) {
|
|
236
416
|
// An active/queued task is not idle. Disarm this cycle and let its next
|
|
237
417
|
// terminal event start a fresh one; repeatedly rearming here would poll
|
|
@@ -248,7 +428,8 @@ export class SessionIdleMonitor {
|
|
|
248
428
|
if (!state || state.emitted)
|
|
249
429
|
return;
|
|
250
430
|
state.emitted = true;
|
|
251
|
-
this.persistState()
|
|
431
|
+
if (!this.persistState())
|
|
432
|
+
return;
|
|
252
433
|
this.options.eventBus.publish({
|
|
253
434
|
type: 'session:idle',
|
|
254
435
|
sessionId,
|
|
@@ -308,6 +489,49 @@ export class SessionIdleMonitor {
|
|
|
308
489
|
return undefined;
|
|
309
490
|
}
|
|
310
491
|
}
|
|
492
|
+
isEligibleSession(session) {
|
|
493
|
+
return !!resolveSessionLoopTarget(session)
|
|
494
|
+
&& (!!session.threadId || !this.options.isCurrentMainSession || this.options.isCurrentMainSession(session));
|
|
495
|
+
}
|
|
496
|
+
async disarmMainSession(sessionId) {
|
|
497
|
+
try {
|
|
498
|
+
const session = await this.options.getSessionById(sessionId);
|
|
499
|
+
if (!session || session.threadId)
|
|
500
|
+
return;
|
|
501
|
+
this.clearTimers(sessionId);
|
|
502
|
+
this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
this.warn(`Failed to disarm main session ${sessionId}`, error);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async activateMainSession(sessionId) {
|
|
509
|
+
try {
|
|
510
|
+
const session = await this.options.getSessionById(sessionId);
|
|
511
|
+
const state = this.sessions.get(sessionId);
|
|
512
|
+
if (!session || session.threadId || !state || !this.isEligibleSession(session))
|
|
513
|
+
return;
|
|
514
|
+
this.clearTimers(sessionId);
|
|
515
|
+
this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
|
|
516
|
+
state.armedAt = this.now();
|
|
517
|
+
state.emitted = false;
|
|
518
|
+
this.persistState();
|
|
519
|
+
this.scheduleSession(sessionId);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.warn(`Failed to activate main session ${sessionId}`, error);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async pruneInactiveMainSession(sessionId) {
|
|
526
|
+
try {
|
|
527
|
+
const session = await this.options.getSessionById(sessionId);
|
|
528
|
+
if (session && !session.threadId && !this.isEligibleSession(session))
|
|
529
|
+
await this.disarmMainSession(sessionId);
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
this.warn(`Failed to inspect main session ${sessionId}`, error);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
311
535
|
loadState() {
|
|
312
536
|
try {
|
|
313
537
|
if (!fs.existsSync(this.options.statePath))
|
|
@@ -330,6 +554,8 @@ export class SessionIdleMonitor {
|
|
|
330
554
|
lastEndedAt,
|
|
331
555
|
emitted,
|
|
332
556
|
...(typeof rawState.encrypted === 'boolean' ? { encrypted: rawState.encrypted } : {}),
|
|
557
|
+
...(typeof rawState.armedAt === 'number' && Number.isFinite(rawState.armedAt) ? { armedAt: rawState.armedAt } : {}),
|
|
558
|
+
...(typeof rawState.idleSeconds === 'number' ? { idleSeconds: rawState.idleSeconds } : {}),
|
|
333
559
|
});
|
|
334
560
|
}
|
|
335
561
|
}
|
|
@@ -344,23 +570,24 @@ export class SessionIdleMonitor {
|
|
|
344
570
|
lastEndedAt: state.lastEndedAt,
|
|
345
571
|
emitted: state.emitted,
|
|
346
572
|
...(typeof state.encrypted === 'boolean' ? { encrypted: state.encrypted } : {}),
|
|
573
|
+
...(state.armedAt !== undefined ? { armedAt: state.armedAt } : {}),
|
|
574
|
+
...(state.idleSeconds !== undefined ? { idleSeconds: state.idleSeconds } : {}),
|
|
347
575
|
};
|
|
348
576
|
}
|
|
349
577
|
try {
|
|
350
578
|
fs.mkdirSync(path.dirname(this.options.statePath), { recursive: true });
|
|
351
579
|
atomicWriteJson(this.options.statePath, { version: 2, sessions });
|
|
580
|
+
return true;
|
|
352
581
|
}
|
|
353
582
|
catch (error) {
|
|
354
583
|
this.warn(`Failed to persist session idle state at ${this.options.statePath}`, error);
|
|
584
|
+
return false;
|
|
355
585
|
}
|
|
356
586
|
}
|
|
357
587
|
warn(message, error) {
|
|
358
588
|
logger.warn(`[SessionIdleMonitor] ${message}${error === undefined ? '' : `: ${error instanceof Error ? error.message : String(error)}`}`);
|
|
359
589
|
}
|
|
360
590
|
}
|
|
361
|
-
function isSupportedSession(session) {
|
|
362
|
-
return session.chatType === 'group' || session.chatType === 'private';
|
|
363
|
-
}
|
|
364
591
|
function toIdleMs(seconds) {
|
|
365
592
|
if (seconds === undefined || !Number.isFinite(seconds) || seconds <= 0)
|
|
366
593
|
return undefined;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { normalizeSessionLoopConfig, resolveSessionLoopTarget, sessionLoopError } from './session-idle-monitor.js';
|
|
1
2
|
import { ensureDir } from '../../utils/atomic-write.js';
|
|
2
3
|
import { resolvePaths } from '../../paths.js';
|
|
3
4
|
import { logger } from '../../utils/logger.js';
|
|
4
5
|
import { encodePath } from '../../utils/cross-platform.js';
|
|
5
6
|
import { readRunnerEventSummary } from './runner-event-summary.js';
|
|
6
7
|
import { isInternalInjectedUserMessage, isToolResultMessage } from './session-transcript.js';
|
|
7
|
-
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
|
|
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';
|
|
8
9
|
import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, normalizeTopicLabel, normalizeTopicStatus, } from './session-mapper.js';
|
|
9
10
|
import { tryParseChannelKey } from '../channel-loader.js';
|
|
10
11
|
import { isSystemControlChannel } from '../system-channels.js';
|
|
@@ -225,7 +226,7 @@ export function pageForkHistory(snapshot, before = snapshot.items.length, limit
|
|
|
225
226
|
};
|
|
226
227
|
}
|
|
227
228
|
/** Metadata keys owned by ordinary topic/session routing writers. */
|
|
228
|
-
const TOPIC_METADATA_PROTECTED_KEYS = new Set(['turnState', 'resumeAt', 'agentSessions']);
|
|
229
|
+
const TOPIC_METADATA_PROTECTED_KEYS = new Set(['turnState', 'resumeAt', 'agentSessions', 'sessionLoop']);
|
|
229
230
|
const TOPIC_BACKEND_SCOPED_KEYS = ['resumeAt', 'agentSessions'];
|
|
230
231
|
function topicMetadataOwnershipError(key) {
|
|
231
232
|
const error = new Error(`topic metadata field is owned by a dedicated updater: ${key}`);
|
|
@@ -260,6 +261,9 @@ export class SessionManager {
|
|
|
260
261
|
identityResolver;
|
|
261
262
|
fileAdapters = new Map();
|
|
262
263
|
sessionEncryptState = new Map();
|
|
264
|
+
/** findSessionFileById 的查找缓存:命中前一律 stat 校验,不作为真相源。 */
|
|
265
|
+
sessionFileIndex = new Map();
|
|
266
|
+
knownChatDirs = null;
|
|
263
267
|
constructor(sessionsDir, eventBus, identityResolver) {
|
|
264
268
|
ensureDir(sessionsDir);
|
|
265
269
|
this.sessionsDir = sessionsDir;
|
|
@@ -626,6 +630,13 @@ export class SessionManager {
|
|
|
626
630
|
appendJsonlDurable(metaPath, file);
|
|
627
631
|
else
|
|
628
632
|
appendJsonl(metaPath, file);
|
|
633
|
+
// 本次快照已落盘;压缩只回收历史行,失败不影响写入结果,原文件保持不变。
|
|
634
|
+
try {
|
|
635
|
+
compactJsonlTail(metaPath, SESSION_META_COMPACT_POLICY);
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
logger.warn(`[SessionManager] meta compaction skipped for ${metaPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
639
|
+
}
|
|
629
640
|
}
|
|
630
641
|
/**
|
|
631
642
|
* Session 持久化的唯一事务原语。所有 session 写操作都应经此,业务层不直接调
|
|
@@ -646,6 +657,15 @@ export class SessionManager {
|
|
|
646
657
|
* @returns 是否真的写入了 .jsonl(去重命中时返回 false)
|
|
647
658
|
*/
|
|
648
659
|
persistSession(session, intent, opts) {
|
|
660
|
+
if (!opts?.sessionLoopMutation) {
|
|
661
|
+
const latest = this.readMetaLatest(this.metaPathForSession(session));
|
|
662
|
+
const metadata = { ...(session.metadata ?? {}) };
|
|
663
|
+
if (latest?.metadata?.sessionLoop !== undefined)
|
|
664
|
+
metadata.sessionLoop = latest.metadata.sessionLoop;
|
|
665
|
+
else
|
|
666
|
+
delete metadata.sessionLoop;
|
|
667
|
+
session.metadata = metadata;
|
|
668
|
+
}
|
|
649
669
|
// Topic session snapshots can be written by synchronous legacy paths
|
|
650
670
|
// (mark/clear processing) after an async /renew has committed. Preserve
|
|
651
671
|
// the newest backend fence unless this call is one of the two explicit
|
|
@@ -807,24 +827,84 @@ export class SessionManager {
|
|
|
807
827
|
}
|
|
808
828
|
return results;
|
|
809
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* 按 sessionId 定位 meta 文件。逐个 id 全量 scanChatDirs 在几百个 chat 目录下
|
|
832
|
+
* 单次约 50ms,启动时批量查询(SessionIdleMonitor 恢复 170+ 会话)会同步阻塞数秒,
|
|
833
|
+
* 所以首次查询时按 readdir 建一份 id→路径索引,之后:
|
|
834
|
+
* - 命中:stat 校验仍在才返回(文件被移进 _trash/目录被迁移时自然失效);
|
|
835
|
+
* - 未命中:先在已知目录里按原规则 stat 探测(覆盖索引之后新建的会话),
|
|
836
|
+
* 再重扫目录列表、只探测新出现的目录。不存在的 id 开销与原实现相当。
|
|
837
|
+
* 目录顺序与「同目录先主文件后 _threads」的优先级保持原实现语义。
|
|
838
|
+
*/
|
|
810
839
|
findSessionFileById(sessionId) {
|
|
811
|
-
const
|
|
812
|
-
|
|
840
|
+
const cached = this.sessionFileIndex.get(sessionId);
|
|
841
|
+
if (cached) {
|
|
842
|
+
try {
|
|
843
|
+
fs.statSync(cached.metaPath);
|
|
844
|
+
return cached;
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
this.sessionFileIndex.delete(sessionId);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!this.knownChatDirs) {
|
|
851
|
+
this.rebuildSessionFileIndex();
|
|
852
|
+
const indexed = this.sessionFileIndex.get(sessionId);
|
|
853
|
+
if (indexed)
|
|
854
|
+
return indexed;
|
|
855
|
+
}
|
|
856
|
+
const probed = this.probeSessionFile(sessionId, this.knownChatDirs);
|
|
857
|
+
if (probed)
|
|
858
|
+
return probed;
|
|
859
|
+
const previous = new Set(this.knownChatDirs);
|
|
860
|
+
this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
|
|
861
|
+
return this.probeSessionFile(sessionId, this.knownChatDirs.filter(dir => !previous.has(dir)));
|
|
862
|
+
}
|
|
863
|
+
probeSessionFile(sessionId, chatDirs) {
|
|
864
|
+
for (const dirPath of chatDirs) {
|
|
813
865
|
const mainPath = path.join(dirPath, `${sessionId}.jsonl`);
|
|
814
866
|
try {
|
|
815
867
|
fs.statSync(mainPath);
|
|
816
|
-
return { chatDir: dirPath, metaPath: mainPath, isThread: false };
|
|
868
|
+
return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: mainPath, isThread: false });
|
|
817
869
|
}
|
|
818
870
|
catch { }
|
|
819
871
|
const threadPath = path.join(dirPath, '_threads', `${sessionId}.jsonl`);
|
|
820
872
|
try {
|
|
821
873
|
fs.statSync(threadPath);
|
|
822
|
-
return { chatDir: dirPath, metaPath: threadPath, isThread: true };
|
|
874
|
+
return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: threadPath, isThread: true });
|
|
823
875
|
}
|
|
824
876
|
catch { }
|
|
825
877
|
}
|
|
826
878
|
return undefined;
|
|
827
879
|
}
|
|
880
|
+
rememberSessionFile(sessionId, found) {
|
|
881
|
+
this.sessionFileIndex.set(sessionId, found);
|
|
882
|
+
return found;
|
|
883
|
+
}
|
|
884
|
+
rebuildSessionFileIndex() {
|
|
885
|
+
this.sessionFileIndex.clear();
|
|
886
|
+
this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
|
|
887
|
+
for (const dirPath of this.knownChatDirs) {
|
|
888
|
+
for (const [dir, isThread] of [[dirPath, false], [path.join(dirPath, '_threads'), true]]) {
|
|
889
|
+
let names;
|
|
890
|
+
try {
|
|
891
|
+
names = fs.readdirSync(dir);
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
for (const name of names) {
|
|
897
|
+
if (!name.endsWith('.jsonl'))
|
|
898
|
+
continue;
|
|
899
|
+
const id = name.slice(0, -'.jsonl'.length);
|
|
900
|
+
// 先到先得:与逐目录探测时的优先级一致。
|
|
901
|
+
if (!this.sessionFileIndex.has(id)) {
|
|
902
|
+
this.sessionFileIndex.set(id, { chatDir: dirPath, metaPath: path.join(dir, name), isThread });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
828
908
|
// ─── Public API ───
|
|
829
909
|
getKnownThreadIds(channel) {
|
|
830
910
|
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
@@ -1308,6 +1388,8 @@ export class SessionManager {
|
|
|
1308
1388
|
if (updates.name !== undefined)
|
|
1309
1389
|
current.name = updates.name;
|
|
1310
1390
|
if (updates.metadata !== undefined) {
|
|
1391
|
+
if (Object.hasOwn(updates.metadata, 'sessionLoop'))
|
|
1392
|
+
throw topicMetadataOwnershipError('sessionLoop');
|
|
1311
1393
|
if (current.threadId) {
|
|
1312
1394
|
for (const key of Object.keys(updates.metadata)) {
|
|
1313
1395
|
if (TOPIC_METADATA_PROTECTED_KEYS.has(key))
|
|
@@ -1337,6 +1419,8 @@ export class SessionManager {
|
|
|
1337
1419
|
return;
|
|
1338
1420
|
const { current } = loaded;
|
|
1339
1421
|
for (const key of Object.keys(patch)) {
|
|
1422
|
+
if (key === 'sessionLoop')
|
|
1423
|
+
throw topicMetadataOwnershipError(key);
|
|
1340
1424
|
if (current.threadId && TOPIC_METADATA_PROTECTED_KEYS.has(key)) {
|
|
1341
1425
|
throw topicMetadataOwnershipError(key);
|
|
1342
1426
|
}
|
|
@@ -1351,6 +1435,21 @@ export class SessionManager {
|
|
|
1351
1435
|
current.metadata = metadata;
|
|
1352
1436
|
this.persistSession(current, 'sync');
|
|
1353
1437
|
}
|
|
1438
|
+
updateSessionLoop(sessionId, config) {
|
|
1439
|
+
const normalized = normalizeSessionLoopConfig(config);
|
|
1440
|
+
if (!normalized)
|
|
1441
|
+
throw sessionLoopError('INVALID_ARGUMENT', 'invalid sessionLoop configuration');
|
|
1442
|
+
return this.mutateLatestTopicSession(sessionId, current => {
|
|
1443
|
+
const target = resolveSessionLoopTarget(current);
|
|
1444
|
+
if (!target || target.level !== 'topic-session')
|
|
1445
|
+
throw sessionLoopError('INVALID_CONTEXT', '话题会话不存在或已不可用');
|
|
1446
|
+
current.metadata = { ...(current.metadata ?? {}), sessionLoop: normalized };
|
|
1447
|
+
const changed = this.persistSession(current, 'none', { sessionLoopMutation: true });
|
|
1448
|
+
if (changed)
|
|
1449
|
+
this.eventBus.publish({ type: 'session:loop-changed', sessionId, timestamp: Date.now() });
|
|
1450
|
+
return current;
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1354
1453
|
/** Persist only the turn coordinator-owned metadata field. */
|
|
1355
1454
|
async updateTurnState(sessionId, turnState) {
|
|
1356
1455
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
@@ -1366,6 +1465,8 @@ export class SessionManager {
|
|
|
1366
1465
|
this.persistSession(current, 'sync', { turnStateMutation: true });
|
|
1367
1466
|
}
|
|
1368
1467
|
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus) {
|
|
1468
|
+
const topicMetadata = { ...(metadata ?? {}) };
|
|
1469
|
+
delete topicMetadata.sessionLoop;
|
|
1369
1470
|
// 使用精确路径(channelType + selfAID)
|
|
1370
1471
|
const chatDir = (channelType && selfAID)
|
|
1371
1472
|
? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
|
|
@@ -1387,7 +1488,7 @@ export class SessionManager {
|
|
|
1387
1488
|
let mutated = false;
|
|
1388
1489
|
if (metadata) {
|
|
1389
1490
|
const creatorPeerId = existing.metadata?.peerId;
|
|
1390
|
-
existing.metadata = { ...(existing.metadata || {}), ...
|
|
1491
|
+
existing.metadata = { ...(existing.metadata || {}), ...topicMetadata };
|
|
1391
1492
|
if (creatorPeerId && existing.metadata)
|
|
1392
1493
|
existing.metadata.peerId = creatorPeerId;
|
|
1393
1494
|
mutated = true;
|
|
@@ -1411,7 +1512,21 @@ export class SessionManager {
|
|
|
1411
1512
|
const mainActive = readJsonFile(path.join(chatDir, 'active.json'));
|
|
1412
1513
|
// 兼容旧字段名
|
|
1413
1514
|
const mainBaseagent = mainActive?.baseagent || mainActive?.agentType;
|
|
1414
|
-
|
|
1515
|
+
// 最终兜底:从 agent config.json 读 active_baseagent(宿主终端下 active.json 可能不存在)
|
|
1516
|
+
const configBaseagent = (() => {
|
|
1517
|
+
if (mainBaseagent || baseagent)
|
|
1518
|
+
return undefined;
|
|
1519
|
+
if (!selfAID)
|
|
1520
|
+
return undefined;
|
|
1521
|
+
try {
|
|
1522
|
+
const cfg = readJsonFile(path.join(resolvePaths().agentsDir, selfAID, 'config.json'));
|
|
1523
|
+
return cfg?.active_baseagent || undefined;
|
|
1524
|
+
}
|
|
1525
|
+
catch {
|
|
1526
|
+
return undefined;
|
|
1527
|
+
}
|
|
1528
|
+
})();
|
|
1529
|
+
const inheritedBaseagent = mainBaseagent || baseagent || configBaseagent;
|
|
1415
1530
|
if (!inheritedBaseagent) {
|
|
1416
1531
|
throw new Error('[SessionManager] getOrCreateThreadSession: baseagent is empty');
|
|
1417
1532
|
}
|
|
@@ -1426,7 +1541,7 @@ export class SessionManager {
|
|
|
1426
1541
|
baseagent: inheritedBaseagent,
|
|
1427
1542
|
sessionKey,
|
|
1428
1543
|
chatType: effectiveChatType,
|
|
1429
|
-
metadata,
|
|
1544
|
+
metadata: topicMetadata,
|
|
1430
1545
|
name: name || '话题会话',
|
|
1431
1546
|
label: normalizeTopicLabel(label),
|
|
1432
1547
|
topicStatus: topicStatus ? normalizeTopicStatus(topicStatus) : 'pending',
|
|
@@ -1671,10 +1786,10 @@ export class SessionManager {
|
|
|
1671
1786
|
return !!session && !session.threadId;
|
|
1672
1787
|
});
|
|
1673
1788
|
}
|
|
1674
|
-
async getThreadSession(channel, channelId, threadId) {
|
|
1789
|
+
async getThreadSession(channel, channelId, threadId, selfAID) {
|
|
1675
1790
|
let chatDir;
|
|
1676
1791
|
try {
|
|
1677
|
-
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
1792
|
+
chatDir = this.resolveChatDirSafe(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
|
|
1678
1793
|
}
|
|
1679
1794
|
catch {
|
|
1680
1795
|
return undefined;
|
|
@@ -1687,9 +1802,6 @@ export class SessionManager {
|
|
|
1687
1802
|
const session = this.readMetaLatest(metaPath);
|
|
1688
1803
|
if (!session)
|
|
1689
1804
|
return undefined;
|
|
1690
|
-
// Do not consult provider-file existence to derive topic binding state;
|
|
1691
|
-
// the topic JSONL boundary is authoritative and may intentionally point
|
|
1692
|
-
// at a backend that needs an explicit /renew or repair operation.
|
|
1693
1805
|
const validSessionId = normalizeBackendSessionId(session.agentSessionId);
|
|
1694
1806
|
return { ...session, agentSessionId: validSessionId };
|
|
1695
1807
|
}
|
|
@@ -1761,6 +1873,8 @@ export class SessionManager {
|
|
|
1761
1873
|
return target;
|
|
1762
1874
|
}
|
|
1763
1875
|
updateMetadata(sessionId, metadata) {
|
|
1876
|
+
if (Object.hasOwn(metadata, 'sessionLoop'))
|
|
1877
|
+
throw topicMetadataOwnershipError('sessionLoop');
|
|
1764
1878
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1765
1879
|
if (!loaded)
|
|
1766
1880
|
return;
|
|
@@ -2123,6 +2237,7 @@ export class SessionManager {
|
|
|
2123
2237
|
delete metadata.agentSessions;
|
|
2124
2238
|
delete metadata.resumeAt;
|
|
2125
2239
|
delete metadata.replyContext;
|
|
2240
|
+
delete metadata.sessionLoop;
|
|
2126
2241
|
metadata.__sessionRenewedAt = Date.now();
|
|
2127
2242
|
const session = {
|
|
2128
2243
|
id: generateSessionId(),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isValidAid } from '../../aun/aid/validation.js';
|
|
2
|
+
export function resolveSkillRefreshArgv(argv) {
|
|
3
|
+
if (argv[0] !== 'skill' || argv[1] !== 'refresh' || argv.length < 3 || argv.length > 4
|
|
4
|
+
|| argv.some(value => typeof value !== 'string'))
|
|
5
|
+
throw new Error('Invalid skill refresh argv');
|
|
6
|
+
const positional = argv.slice(2).filter(value => value !== '--json');
|
|
7
|
+
if (positional.length !== 1)
|
|
8
|
+
throw new Error('Specify one Agent AID');
|
|
9
|
+
const aid = positional[0].replace(/^@/, '');
|
|
10
|
+
if (!isValidAid(aid))
|
|
11
|
+
throw new Error('Invalid Agent AID');
|
|
12
|
+
return aid;
|
|
13
|
+
}
|