evolcore 0.0.32 → 0.0.33
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 +24 -0
- package/bin/install-codex-managed-hooks.mjs +124 -37
- package/dist/agents/claude-runner.js +29 -2
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +76 -44
- package/dist/agents/codex-runner.js +23 -19
- package/dist/agents/ecagent-runner.js +5 -13
- 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 +28 -2
- package/dist/channels/daemon.js +257 -2
- package/dist/cli/agent-command.js +58 -24
- package/dist/cli/agent.js +1 -1
- package/dist/cli/aun-commands.js +365 -29
- package/dist/cli/contact.js +8 -2
- package/dist/cli/daemon-commands.js +81 -12
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +81 -64
- package/dist/cli/init.js +2 -2
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +8 -1
- 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 +620 -0
- package/dist/cli/trigger-command.js +4 -2
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +2 -3
- package/dist/config/config-manager.js +7 -2
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +15 -0
- package/dist/core/auth/agent-delegation.js +25 -1
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/operation-authorizer.js +24 -1
- package/dist/core/auth/operation-catalog.js +33 -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/command-handler.js +60 -24
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +48 -1
- package/dist/core/command/menu-handler.js +92 -16
- package/dist/core/command/menu-protocol.js +24 -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 +193 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +139 -2
- package/dist/core/memory/consolidation-context.js +49 -16
- package/dist/core/memory/consolidation-ops-records.js +35 -0
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +174 -180
- package/dist/core/message/aun-ipc-sender.js +161 -6
- package/dist/core/message/im-renderer.js +149 -6
- package/dist/core/message/message-bridge.js +13 -3
- 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 +349 -67
- 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 +126 -7
- package/dist/core/session/session-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +54 -9
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +4 -1
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- 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 +777 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +8 -5
- package/dist/ipc.js +89 -10
- package/dist/paths.js +23 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/trigger/authorization.js +57 -28
- package/dist/trigger/feedback.js +7 -1
- package/dist/trigger/parser.js +63 -1
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/error-utils.js +22 -1
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +39 -28
- package/kits/docs/evolcore/agent.md +19 -4
- package/kits/docs/evolcore/ec-capabilities.md +4 -0
- package/kits/docs/evolcore/group-collaboration.md +3 -5
- package/kits/docs/evolcore/group.md +3 -4
- package/kits/docs/evolcore/memory-consolidation.md +1 -1
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/msg.md +5 -8
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/prompt-loading-architecture.md +1 -0
- package/kits/eck_manifest.json +61 -26
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/07-memory-and-thread.md +202 -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 +15 -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 +5 -0
- package/kits/templates/memory-consolidation/generalize.md +5 -0
- package/kits/templates/memory-consolidation/profile.md +5 -0
- package/kits/templates/memory-consolidation/promote.md +5 -0
- 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 +66 -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/reply.md +3 -2
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +3 -1
- 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,3 +1,4 @@
|
|
|
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';
|
|
@@ -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}`);
|
|
@@ -646,6 +647,15 @@ export class SessionManager {
|
|
|
646
647
|
* @returns 是否真的写入了 .jsonl(去重命中时返回 false)
|
|
647
648
|
*/
|
|
648
649
|
persistSession(session, intent, opts) {
|
|
650
|
+
if (!opts?.sessionLoopMutation) {
|
|
651
|
+
const latest = this.readMetaLatest(this.metaPathForSession(session));
|
|
652
|
+
const metadata = { ...(session.metadata ?? {}) };
|
|
653
|
+
if (latest?.metadata?.sessionLoop !== undefined)
|
|
654
|
+
metadata.sessionLoop = latest.metadata.sessionLoop;
|
|
655
|
+
else
|
|
656
|
+
delete metadata.sessionLoop;
|
|
657
|
+
session.metadata = metadata;
|
|
658
|
+
}
|
|
649
659
|
// Topic session snapshots can be written by synchronous legacy paths
|
|
650
660
|
// (mark/clear processing) after an async /renew has committed. Preserve
|
|
651
661
|
// the newest backend fence unless this call is one of the two explicit
|
|
@@ -1308,6 +1318,8 @@ export class SessionManager {
|
|
|
1308
1318
|
if (updates.name !== undefined)
|
|
1309
1319
|
current.name = updates.name;
|
|
1310
1320
|
if (updates.metadata !== undefined) {
|
|
1321
|
+
if (Object.hasOwn(updates.metadata, 'sessionLoop'))
|
|
1322
|
+
throw topicMetadataOwnershipError('sessionLoop');
|
|
1311
1323
|
if (current.threadId) {
|
|
1312
1324
|
for (const key of Object.keys(updates.metadata)) {
|
|
1313
1325
|
if (TOPIC_METADATA_PROTECTED_KEYS.has(key))
|
|
@@ -1337,6 +1349,8 @@ export class SessionManager {
|
|
|
1337
1349
|
return;
|
|
1338
1350
|
const { current } = loaded;
|
|
1339
1351
|
for (const key of Object.keys(patch)) {
|
|
1352
|
+
if (key === 'sessionLoop')
|
|
1353
|
+
throw topicMetadataOwnershipError(key);
|
|
1340
1354
|
if (current.threadId && TOPIC_METADATA_PROTECTED_KEYS.has(key)) {
|
|
1341
1355
|
throw topicMetadataOwnershipError(key);
|
|
1342
1356
|
}
|
|
@@ -1351,6 +1365,21 @@ export class SessionManager {
|
|
|
1351
1365
|
current.metadata = metadata;
|
|
1352
1366
|
this.persistSession(current, 'sync');
|
|
1353
1367
|
}
|
|
1368
|
+
updateSessionLoop(sessionId, config) {
|
|
1369
|
+
const normalized = normalizeSessionLoopConfig(config);
|
|
1370
|
+
if (!normalized)
|
|
1371
|
+
throw sessionLoopError('INVALID_ARGUMENT', 'invalid sessionLoop configuration');
|
|
1372
|
+
return this.mutateLatestTopicSession(sessionId, current => {
|
|
1373
|
+
const target = resolveSessionLoopTarget(current);
|
|
1374
|
+
if (!target || target.level !== 'topic-session')
|
|
1375
|
+
throw sessionLoopError('INVALID_CONTEXT', '话题会话不存在或已不可用');
|
|
1376
|
+
current.metadata = { ...(current.metadata ?? {}), sessionLoop: normalized };
|
|
1377
|
+
const changed = this.persistSession(current, 'none', { sessionLoopMutation: true });
|
|
1378
|
+
if (changed)
|
|
1379
|
+
this.eventBus.publish({ type: 'session:loop-changed', sessionId, timestamp: Date.now() });
|
|
1380
|
+
return current;
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1354
1383
|
/** Persist only the turn coordinator-owned metadata field. */
|
|
1355
1384
|
async updateTurnState(sessionId, turnState) {
|
|
1356
1385
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
@@ -1366,6 +1395,8 @@ export class SessionManager {
|
|
|
1366
1395
|
this.persistSession(current, 'sync', { turnStateMutation: true });
|
|
1367
1396
|
}
|
|
1368
1397
|
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus) {
|
|
1398
|
+
const topicMetadata = { ...(metadata ?? {}) };
|
|
1399
|
+
delete topicMetadata.sessionLoop;
|
|
1369
1400
|
// 使用精确路径(channelType + selfAID)
|
|
1370
1401
|
const chatDir = (channelType && selfAID)
|
|
1371
1402
|
? (() => { 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 +1418,7 @@ export class SessionManager {
|
|
|
1387
1418
|
let mutated = false;
|
|
1388
1419
|
if (metadata) {
|
|
1389
1420
|
const creatorPeerId = existing.metadata?.peerId;
|
|
1390
|
-
existing.metadata = { ...(existing.metadata || {}), ...
|
|
1421
|
+
existing.metadata = { ...(existing.metadata || {}), ...topicMetadata };
|
|
1391
1422
|
if (creatorPeerId && existing.metadata)
|
|
1392
1423
|
existing.metadata.peerId = creatorPeerId;
|
|
1393
1424
|
mutated = true;
|
|
@@ -1411,7 +1442,21 @@ export class SessionManager {
|
|
|
1411
1442
|
const mainActive = readJsonFile(path.join(chatDir, 'active.json'));
|
|
1412
1443
|
// 兼容旧字段名
|
|
1413
1444
|
const mainBaseagent = mainActive?.baseagent || mainActive?.agentType;
|
|
1414
|
-
|
|
1445
|
+
// 最终兜底:从 agent config.json 读 active_baseagent(宿主终端下 active.json 可能不存在)
|
|
1446
|
+
const configBaseagent = (() => {
|
|
1447
|
+
if (mainBaseagent || baseagent)
|
|
1448
|
+
return undefined;
|
|
1449
|
+
if (!selfAID)
|
|
1450
|
+
return undefined;
|
|
1451
|
+
try {
|
|
1452
|
+
const cfg = readJsonFile(path.join(resolvePaths().agentsDir, selfAID, 'config.json'));
|
|
1453
|
+
return cfg?.active_baseagent || undefined;
|
|
1454
|
+
}
|
|
1455
|
+
catch {
|
|
1456
|
+
return undefined;
|
|
1457
|
+
}
|
|
1458
|
+
})();
|
|
1459
|
+
const inheritedBaseagent = mainBaseagent || baseagent || configBaseagent;
|
|
1415
1460
|
if (!inheritedBaseagent) {
|
|
1416
1461
|
throw new Error('[SessionManager] getOrCreateThreadSession: baseagent is empty');
|
|
1417
1462
|
}
|
|
@@ -1426,7 +1471,7 @@ export class SessionManager {
|
|
|
1426
1471
|
baseagent: inheritedBaseagent,
|
|
1427
1472
|
sessionKey,
|
|
1428
1473
|
chatType: effectiveChatType,
|
|
1429
|
-
metadata,
|
|
1474
|
+
metadata: topicMetadata,
|
|
1430
1475
|
name: name || '话题会话',
|
|
1431
1476
|
label: normalizeTopicLabel(label),
|
|
1432
1477
|
topicStatus: topicStatus ? normalizeTopicStatus(topicStatus) : 'pending',
|
|
@@ -1671,10 +1716,10 @@ export class SessionManager {
|
|
|
1671
1716
|
return !!session && !session.threadId;
|
|
1672
1717
|
});
|
|
1673
1718
|
}
|
|
1674
|
-
async getThreadSession(channel, channelId, threadId) {
|
|
1719
|
+
async getThreadSession(channel, channelId, threadId, selfAID) {
|
|
1675
1720
|
let chatDir;
|
|
1676
1721
|
try {
|
|
1677
|
-
chatDir = this.resolveChatDirSafe(channel, channelId);
|
|
1722
|
+
chatDir = this.resolveChatDirSafe(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
|
|
1678
1723
|
}
|
|
1679
1724
|
catch {
|
|
1680
1725
|
return undefined;
|
|
@@ -1687,9 +1732,6 @@ export class SessionManager {
|
|
|
1687
1732
|
const session = this.readMetaLatest(metaPath);
|
|
1688
1733
|
if (!session)
|
|
1689
1734
|
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
1735
|
const validSessionId = normalizeBackendSessionId(session.agentSessionId);
|
|
1694
1736
|
return { ...session, agentSessionId: validSessionId };
|
|
1695
1737
|
}
|
|
@@ -1761,6 +1803,8 @@ export class SessionManager {
|
|
|
1761
1803
|
return target;
|
|
1762
1804
|
}
|
|
1763
1805
|
updateMetadata(sessionId, metadata) {
|
|
1806
|
+
if (Object.hasOwn(metadata, 'sessionLoop'))
|
|
1807
|
+
throw topicMetadataOwnershipError('sessionLoop');
|
|
1764
1808
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1765
1809
|
if (!loaded)
|
|
1766
1810
|
return;
|
|
@@ -2123,6 +2167,7 @@ export class SessionManager {
|
|
|
2123
2167
|
delete metadata.agentSessions;
|
|
2124
2168
|
delete metadata.resumeAt;
|
|
2125
2169
|
delete metadata.replyContext;
|
|
2170
|
+
delete metadata.sessionLoop;
|
|
2126
2171
|
metadata.__sessionRenewedAt = Date.now();
|
|
2127
2172
|
const session = {
|
|
2128
2173
|
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
|
+
}
|
|
@@ -42,6 +42,9 @@ function generateOneIndex(indexDir, filename, skills, label) {
|
|
|
42
42
|
const sep = path.sep;
|
|
43
43
|
const header = `[skills-${label}] 本 Agent 已启用 ${skills.length} 个${label === 'builtin' ? '内置' : '本机'}技能。
|
|
44
44
|
|
|
45
|
+
加载方式: EC 技能用 Read 工具读取上面对应的 SKILL.md 全文后按内容执行;
|
|
46
|
+
它们不是宿主工具自带的技能或斜杠命令,用 Skill/斜杠命令加载会报 Unknown skill/未知命令。
|
|
47
|
+
|
|
45
48
|
路径规则:
|
|
46
49
|
根目录: ${rootDir}
|
|
47
50
|
技能目录 = 根目录 + "${sep}" + 技能名
|
|
@@ -66,7 +69,7 @@ function generateOneIndex(indexDir, filename, skills, label) {
|
|
|
66
69
|
* 直接按目录读,不走 discoverSkillDirectories——后者按父目录扫描再按 name 匹配,
|
|
67
70
|
* 同名技能(本机覆盖内置)会读到另一份的 description。
|
|
68
71
|
*/
|
|
69
|
-
function readSkillDescription(skillDir) {
|
|
72
|
+
export function readSkillDescription(skillDir) {
|
|
70
73
|
try {
|
|
71
74
|
const text = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
|
|
72
75
|
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
@@ -2,6 +2,28 @@ import { generateSkillIndexes } from './index-generator.js';
|
|
|
2
2
|
import { globalSkillWatcher } from './global-watcher.js';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
4
|
import { invalidateKitCache } from '../../eck/kit-renderer.js';
|
|
5
|
+
import { isSkillWatchEnabled } from '../../config-store.js';
|
|
6
|
+
/**
|
|
7
|
+
* 监控开关(daemon.json `skills.watch`,默认 false),daemon 启动时读一次。
|
|
8
|
+
*
|
|
9
|
+
* 关闭时不创建任何 chokidar watcher:Windows 上 chokidar 每个文件/目录各占一个
|
|
10
|
+
* 目录句柄(本机实测 ~/.evolcore/skills 约 3400 个),子目录因此无法被资源管理器
|
|
11
|
+
* 删除或重命名。关闭后改完技能需执行 `ec skill refresh <aid>`(refreshSkillIndex)。
|
|
12
|
+
* 监控与手动刷新互不冲突,打开开关后两者可同时使用。
|
|
13
|
+
*/
|
|
14
|
+
let watchEnabled;
|
|
15
|
+
function skillWatchEnabled() {
|
|
16
|
+
if (watchEnabled === undefined) {
|
|
17
|
+
try {
|
|
18
|
+
watchEnabled = isSkillWatchEnabled();
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
logger.warn(`[skill] 读取 skills.watch 失败,按关闭处理: ${e instanceof Error ? e.message : String(e)}`);
|
|
22
|
+
watchEnabled = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return watchEnabled;
|
|
26
|
+
}
|
|
5
27
|
/**
|
|
6
28
|
* daemon 启动时,启动全局监控器。
|
|
7
29
|
*
|
|
@@ -11,6 +33,10 @@ import { invalidateKitCache } from '../../eck/kit-renderer.js';
|
|
|
11
33
|
* - agent 收到消息需要组装系统上下文时,检查并按需生成索引
|
|
12
34
|
*/
|
|
13
35
|
export function startSkillWatcher() {
|
|
36
|
+
if (!skillWatchEnabled()) {
|
|
37
|
+
logger.info('[skill] 技能目录监控已关闭(daemon.json skills.watch=false),改动技能后请执行 ec skill refresh <aid>');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
14
40
|
// 启动全局监控器(监控内置和本机技能目录)
|
|
15
41
|
globalSkillWatcher.start();
|
|
16
42
|
// 注册防抖回调:内置和本机技能变化时标记所有 agent 为脏并失效缓存
|
|
@@ -36,6 +62,8 @@ export function startSkillWatcher() {
|
|
|
36
62
|
* 重复调用会 re-arm debounce timer 造成 2 秒自激循环。
|
|
37
63
|
*/
|
|
38
64
|
export function watchAgentSkills(aid) {
|
|
65
|
+
if (!skillWatchEnabled())
|
|
66
|
+
return;
|
|
39
67
|
globalSkillWatcher.watchAgent(aid);
|
|
40
68
|
globalSkillWatcher.onDebouncedChange(`agent:${aid}`, () => {
|
|
41
69
|
logger.info(`[skill] Agent ${aid} 技能目录有变化,标记为脏状态并失效缓存`);
|
|
@@ -65,6 +93,19 @@ export function ensureSkillIndexUpToDate(agentDir, aid) {
|
|
|
65
93
|
}
|
|
66
94
|
}
|
|
67
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* 手动刷新指定 agent 的技能索引(`ec skill refresh <aid>` 的 daemon 侧实现)。
|
|
98
|
+
*
|
|
99
|
+
* 与懒加载入口不同:无条件重建,失败直接抛出(由 IPC 返回给调用方),
|
|
100
|
+
* 成功后清掉该 agent 的脏标记,避免下一轮对话再重建一次。
|
|
101
|
+
*/
|
|
102
|
+
export function refreshSkillIndex(agentDir, aid) {
|
|
103
|
+
generateSkillIndexes(agentDir);
|
|
104
|
+
globalSkillWatcher.markAgentChecked(aid);
|
|
105
|
+
globalSkillWatcher.markAgentClean(aid);
|
|
106
|
+
invalidateKitCache();
|
|
107
|
+
logger.info(`[skill] 已手动刷新 agent ${aid} 的技能索引`);
|
|
108
|
+
}
|
|
68
109
|
/**
|
|
69
110
|
* 停止全局监控器(daemon 关闭时调用)
|
|
70
111
|
*/
|