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,4 +1,4 @@
|
|
|
1
|
-
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable,
|
|
1
|
+
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable, } from '../core/model/model-catalog.js';
|
|
2
2
|
// Ecagent/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
|
|
3
3
|
import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
|
|
4
4
|
import fs from 'fs';
|
|
@@ -762,15 +762,7 @@ export class EcagentRunner {
|
|
|
762
762
|
return catalog.models.map(m => m.id);
|
|
763
763
|
}
|
|
764
764
|
async getAvailableEfforts(model) {
|
|
765
|
-
|
|
766
|
-
const efforts = catalogEfforts(catalog, model, 'ecagent');
|
|
767
|
-
if (efforts.length > 0)
|
|
768
|
-
return efforts;
|
|
769
|
-
// The embedded transport advertises reasoning support for every model it
|
|
770
|
-
// constructs. Older gateways may omit reasoningLevels from /models; keep
|
|
771
|
-
// menu.options aligned with the actual runner capability in that case.
|
|
772
|
-
const entry = catalog.models.find(item => item.id === resolveCatalogModel(catalog, model));
|
|
773
|
-
return entry && entry.reasoningLevels === undefined ? [...TRANSPORT_EFFORTS.ecagent] : efforts;
|
|
765
|
+
return catalogEfforts(await this.getModelCatalog(), model, 'ecagent');
|
|
774
766
|
}
|
|
775
767
|
registerStream(key, stream) { this.activeStreams.set(key, stream); }
|
|
776
768
|
cleanupStream(key) {
|
|
@@ -917,7 +909,7 @@ export class EcagentRunner {
|
|
|
917
909
|
if (pauseResult === 'cancelled')
|
|
918
910
|
return { block: true, reason: '工具调用已取消' };
|
|
919
911
|
}
|
|
920
|
-
const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
|
|
912
|
+
const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input, stats.numTurns + 1);
|
|
921
913
|
return lifecycle.cancelled || signal?.aborted ? { block: true, reason: '工具调用已取消' } : decision;
|
|
922
914
|
});
|
|
923
915
|
const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
|
|
@@ -1175,7 +1167,7 @@ export class EcagentRunner {
|
|
|
1175
1167
|
model: model.id, effort,
|
|
1176
1168
|
};
|
|
1177
1169
|
}
|
|
1178
|
-
async authorizeTool(sessionId, projectPath, mode, toolName, input) {
|
|
1170
|
+
async authorizeTool(sessionId, projectPath, mode, toolName, input, turn) {
|
|
1179
1171
|
const permissionContext = this.permissionContexts.get(sessionId);
|
|
1180
1172
|
if (mode === 'fullaccess') {
|
|
1181
1173
|
return hasTrustedFullAccessContext(permissionContext)
|
|
@@ -1220,7 +1212,7 @@ export class EcagentRunner {
|
|
|
1220
1212
|
logger.warn(`[EcagentRunner] failed to record blocked operation: session=${sessionId} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1221
1213
|
}
|
|
1222
1214
|
};
|
|
1223
|
-
const policy = permissionContext?.policyHook?.(toolName, input);
|
|
1215
|
+
const policy = permissionContext?.policyHook?.(toolName, input, { turn });
|
|
1224
1216
|
if (policy?.block) {
|
|
1225
1217
|
await recordBlockedOperation(policy.policyCode ?? 'session_policy_hook');
|
|
1226
1218
|
return { block: true, reason: policy.reason || '当前会话策略拒绝此工具调用' };
|
|
@@ -117,7 +117,6 @@ async function tryDaemonGroupSend(args, payload, runtimeContext, file) {
|
|
|
117
117
|
mentions: args.mentions,
|
|
118
118
|
encrypt: args.encrypt,
|
|
119
119
|
thread,
|
|
120
|
-
returnPolicy: args.returnPolicy,
|
|
121
120
|
originSessionId: runtimeContext.sessionId,
|
|
122
121
|
originMessageId: runtimeContext.messageId,
|
|
123
122
|
delegationToken: readDelegationCredential(),
|
package/dist/aun/msg/history.js
CHANGED
|
@@ -171,6 +171,7 @@ function readHistoryScopes(input) {
|
|
|
171
171
|
.filter(({ entry, sessionIds }) => sessionIds === undefined
|
|
172
172
|
|| (typeof entry.sessionId === 'string' && sessionIds.has(entry.sessionId)))
|
|
173
173
|
.filter(({ entry }) => input.direction === 'all' || entry.dir === input.direction)
|
|
174
|
+
.filter(({ entry }) => input.args.threadId === undefined || entry.threadId === input.args.threadId)
|
|
174
175
|
.filter(({ entry }) => input.args.after === undefined || entry.ts > input.args.after)
|
|
175
176
|
.filter(({ entry }) => input.args.before === undefined || entry.ts < input.args.before)
|
|
176
177
|
.sort((left, right) => left.entry.ts - right.entry.ts || left.scope - right.scope)
|
package/dist/aun/msg/p2p.js
CHANGED
|
@@ -134,7 +134,6 @@ async function tryDaemonMsgSend(args, payload, runtimeContext, file) {
|
|
|
134
134
|
...(payload ? { payload } : { file }),
|
|
135
135
|
encrypt: args.encrypt,
|
|
136
136
|
thread,
|
|
137
|
-
returnPolicy: args.returnPolicy,
|
|
138
137
|
handoffContext: args.handoffContext,
|
|
139
138
|
originSessionId: runtimeContext.sessionId,
|
|
140
139
|
originMessageId: runtimeContext.messageId,
|
package/dist/aun/outbox.js
CHANGED
|
@@ -64,6 +64,24 @@ function isExpired(entry) {
|
|
|
64
64
|
return false;
|
|
65
65
|
return Date.now() - entry.ts > entry.ttl;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* A process-scoped entry whose enqueuing process is gone. Replaying it would
|
|
69
|
+
* deliver stale live feedback — typically the "restarting, back in ~5s" notice
|
|
70
|
+
* that a successful restart has already superseded with its own confirmation.
|
|
71
|
+
*
|
|
72
|
+
* A stale entry is not `isExpired()`: it needs `drainOnce` to retire it with a
|
|
73
|
+
* reason of its own, otherwise the send log would report a semantic drop as a
|
|
74
|
+
* timeout.
|
|
75
|
+
*/
|
|
76
|
+
export function isStaleProcessScoped(entry) {
|
|
77
|
+
if (!entry.processScoped)
|
|
78
|
+
return false;
|
|
79
|
+
// A record without a pid cannot prove same-process ownership. Treat it as
|
|
80
|
+
// replayable so the ordinary route/expiry rules stay in charge.
|
|
81
|
+
if (typeof entry.enqueuedByPid !== 'number')
|
|
82
|
+
return false;
|
|
83
|
+
return entry.enqueuedByPid !== process.pid;
|
|
84
|
+
}
|
|
67
85
|
export function isDeliveryForChannel(value, channelId) {
|
|
68
86
|
return isDeliveryTargetForChannel(value, channelId);
|
|
69
87
|
}
|
|
@@ -129,6 +147,7 @@ export function enqueue(aid, opts) {
|
|
|
129
147
|
delivery,
|
|
130
148
|
dedupeKey: opts.dedupeKey,
|
|
131
149
|
critical: opts.critical,
|
|
150
|
+
...(opts.processScoped ? { processScoped: true, enqueuedByPid: process.pid } : {}),
|
|
132
151
|
type: opts.type,
|
|
133
152
|
contentKind: opts.contentKind,
|
|
134
153
|
payload: opts.payload,
|
|
@@ -273,6 +292,19 @@ function markExpired(entry) {
|
|
|
273
292
|
code: entry.lastErrorCode,
|
|
274
293
|
};
|
|
275
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Drop a process-scoped entry left behind by a previous process. The terminal
|
|
297
|
+
* record is retained so the send log still shows why it never went out.
|
|
298
|
+
*/
|
|
299
|
+
function markSupersededProcessScoped(entry) {
|
|
300
|
+
entry.lastError = 'process-scoped delivery superseded by a new daemon process';
|
|
301
|
+
entry.lastErrorCode = 'OUTBOX_PROCESS_SUPERSEDED';
|
|
302
|
+
entry.terminal = {
|
|
303
|
+
at: Date.now(),
|
|
304
|
+
error: entry.lastError,
|
|
305
|
+
code: entry.lastErrorCode,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
276
308
|
/** Mark a durable entry terminal while keeping its reason for diagnostics. */
|
|
277
309
|
export function markTerminal(aid, id, failure, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
|
|
278
310
|
const entries = readEntries(aid, queue);
|
|
@@ -336,6 +368,7 @@ async function drainOnce(aid, sender, queue) {
|
|
|
336
368
|
let expired = 0;
|
|
337
369
|
let failed = 0;
|
|
338
370
|
let permanent = 0;
|
|
371
|
+
let superseded = 0;
|
|
339
372
|
const remaining = [];
|
|
340
373
|
for (const entry of entries) {
|
|
341
374
|
// Cancellation or another sender may have changed an
|
|
@@ -350,6 +383,16 @@ async function drainOnce(aid, sender, queue) {
|
|
|
350
383
|
}
|
|
351
384
|
Object.assign(entry, currentEntry);
|
|
352
385
|
}
|
|
386
|
+
if (isStaleProcessScoped(entry)) {
|
|
387
|
+
// Deliberately not counted as `expired`: this is a semantic drop, not a
|
|
388
|
+
// timeout, and the drained-line diagnostic should not conflate them.
|
|
389
|
+
// The terminal record also retires the entry from `hasPending()`, so a
|
|
390
|
+
// later drain does not rediscover it.
|
|
391
|
+
superseded++;
|
|
392
|
+
markSupersededProcessScoped(entry);
|
|
393
|
+
remaining.push(entry);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
353
396
|
if (isExpired(entry)) {
|
|
354
397
|
expired++;
|
|
355
398
|
markExpired(entry);
|
|
@@ -457,7 +500,13 @@ async function drainOnce(aid, sender, queue) {
|
|
|
457
500
|
}];
|
|
458
501
|
});
|
|
459
502
|
writeEntries(aid, retained, queue);
|
|
460
|
-
return {
|
|
503
|
+
return {
|
|
504
|
+
sent,
|
|
505
|
+
expired,
|
|
506
|
+
failed,
|
|
507
|
+
...(permanent > 0 ? { permanent } : {}),
|
|
508
|
+
...(superseded > 0 ? { superseded } : {}),
|
|
509
|
+
};
|
|
461
510
|
}
|
|
462
511
|
export async function drain(aid, sender, queue = 'default') {
|
|
463
512
|
const drainKey = `${queue}:${aid}`;
|
package/dist/channels/aun.js
CHANGED
|
@@ -4413,6 +4413,9 @@ export class AUNChannel {
|
|
|
4413
4413
|
};
|
|
4414
4414
|
if (evolCommand)
|
|
4415
4415
|
base.ext = { evolCommand };
|
|
4416
|
+
// Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
|
|
4417
|
+
if (context?.debug)
|
|
4418
|
+
base.debug = { ...context.debug, sentAt: localTimestamp() };
|
|
4416
4419
|
return withAunAttention(base);
|
|
4417
4420
|
}
|
|
4418
4421
|
async sendReliableStructured(channelId, payload, context, logText) {
|
|
@@ -4496,6 +4499,9 @@ export class AUNChannel {
|
|
|
4496
4499
|
delivery,
|
|
4497
4500
|
dedupeKey: operationId,
|
|
4498
4501
|
critical: routedContext.metadata?.criticalDelivery === true,
|
|
4502
|
+
// Live feedback about the running process (for example the pre-restart
|
|
4503
|
+
// notice) must never be replayed by the process that replaces it.
|
|
4504
|
+
processScoped: routedContext.metadata?.processScopedDelivery === true,
|
|
4499
4505
|
type: 'text',
|
|
4500
4506
|
text: finalText,
|
|
4501
4507
|
context: routedContext,
|
|
@@ -4565,6 +4571,16 @@ export class AUNChannel {
|
|
|
4565
4571
|
};
|
|
4566
4572
|
}
|
|
4567
4573
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
4574
|
+
/**
|
|
4575
|
+
* Agent 经 CLI 委托发送(ec msg send / ec group send)时占用本轮序号,
|
|
4576
|
+
* 与回复路径共用 sentCount,保证同一轮内 seqInTurn 连续。
|
|
4577
|
+
*/
|
|
4578
|
+
nextSeqInTurn(channelId, threadId) {
|
|
4579
|
+
const turnKey = `${channelId}\0${threadId ?? ''}`;
|
|
4580
|
+
const seqInTurn = (this.sentCount.get(turnKey) || 0) + 1;
|
|
4581
|
+
this.sentCount.set(turnKey, seqInTurn);
|
|
4582
|
+
return seqInTurn;
|
|
4583
|
+
}
|
|
4568
4584
|
async sendDaemonMsg(args) {
|
|
4569
4585
|
if (!args.to) {
|
|
4570
4586
|
return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
|
|
@@ -5271,6 +5287,9 @@ export class AUNChannel {
|
|
|
5271
5287
|
delete payloadWithChatmode.thread_id;
|
|
5272
5288
|
if (context?.threadId && !payloadWithChatmode.thread_id)
|
|
5273
5289
|
payloadWithChatmode.thread_id = context.threadId;
|
|
5290
|
+
// Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
|
|
5291
|
+
if (context?.debug)
|
|
5292
|
+
payloadWithChatmode.debug = { ...context.debug, sentAt: localTimestamp() };
|
|
5274
5293
|
const params = {
|
|
5275
5294
|
context: { type: 'task', id: taskId },
|
|
5276
5295
|
payload: this.limitThoughtPayload(payloadWithChatmode),
|
|
@@ -5847,8 +5866,12 @@ export class AUNChannel {
|
|
|
5847
5866
|
}
|
|
5848
5867
|
return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
|
|
5849
5868
|
}, queue);
|
|
5850
|
-
if (result.sent > 0 || result.expired > 0 || result.permanent) {
|
|
5851
|
-
|
|
5869
|
+
if (result.sent > 0 || result.expired > 0 || result.permanent || result.superseded) {
|
|
5870
|
+
const superseded = result.superseded ? ` superseded=${result.superseded}` : '';
|
|
5871
|
+
logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}${superseded}`);
|
|
5872
|
+
}
|
|
5873
|
+
if (result.superseded) {
|
|
5874
|
+
logger.info(`${this.logPrefix()} ${queue} outbox: dropped ${result.superseded} process-scoped entr${result.superseded === 1 ? 'y' : 'ies'} left by a previous daemon process`);
|
|
5852
5875
|
}
|
|
5853
5876
|
};
|
|
5854
5877
|
await drainQueue('default');
|
|
@@ -5962,6 +5985,9 @@ export class AUNChannel {
|
|
|
5962
5985
|
statusPayload.chatmode = context.metadata.chatmode;
|
|
5963
5986
|
if (context?.replyToMessageId)
|
|
5964
5987
|
statusPayload.ref_message_id = context.replyToMessageId;
|
|
5988
|
+
// Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
|
|
5989
|
+
if (context?.debug)
|
|
5990
|
+
statusPayload.debug = { ...context.debug, sentAt: localTimestamp() };
|
|
5965
5991
|
const notifyOptions = { ttlMs: 60_000 };
|
|
5966
5992
|
if (delivery.chatType === 'group')
|
|
5967
5993
|
notifyOptions.groupId = delivery.groupId;
|
package/dist/channels/daemon.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appendExpireEntry, recordNextLaunchCleanup, cleanupPreviousByKind, runExpireCleanup, cleanupSessionRecord, } from '../core/background-session/session-cleanup.js';
|
|
1
2
|
import { ExecutionTimeoutError } from '../utils/error-utils.js';
|
|
2
3
|
import { normalizeExecutionMode } from '../trigger/validation.js';
|
|
3
4
|
import { resolvePeerRoleDetail } from '../config/peer-role-resolver.js';
|
|
@@ -17,6 +18,7 @@ export class DaemonChannel {
|
|
|
17
18
|
thread: true,
|
|
18
19
|
};
|
|
19
20
|
pending = new Map();
|
|
21
|
+
bgPending = new Map();
|
|
20
22
|
watchdogMs;
|
|
21
23
|
totalExecutionMs;
|
|
22
24
|
constructor(sessionManager, messageQueue, opts = {}) {
|
|
@@ -26,6 +28,8 @@ export class DaemonChannel {
|
|
|
26
28
|
this.totalExecutionMs = normalizeTotalExecutionMs(opts.totalExecutionMs);
|
|
27
29
|
}
|
|
28
30
|
async converse(prompt, ctx) {
|
|
31
|
+
// 启动前跑一次 expire 扫描(成本低,只读少量小文件)
|
|
32
|
+
void runExpireCleanup(this.sessionManager).catch(() => { });
|
|
29
33
|
const runId = ctx.runId;
|
|
30
34
|
const pendingId = ctx.attemptId ?? runId;
|
|
31
35
|
const messageId = pendingId;
|
|
@@ -41,6 +45,11 @@ export class DaemonChannel {
|
|
|
41
45
|
? undefined
|
|
42
46
|
: setTimeout(() => this.totalExecutionTimeout(pendingId, remainingExecutionMs), remainingExecutionMs);
|
|
43
47
|
totalExecutionTimer?.unref?.();
|
|
48
|
+
// trigger 清理策略:once → expire(3天),其他 → expire(30天)
|
|
49
|
+
const isOnce = ctx.trigger.source?.type === 'once';
|
|
50
|
+
const triggerCleanupPolicy = isOnce
|
|
51
|
+
? { mode: 'expire', ttlMs: 3 * 24 * 3600 * 1000 }
|
|
52
|
+
: { mode: 'expire', ttlMs: 30 * 24 * 3600 * 1000 };
|
|
44
53
|
this.pending.set(pendingId, {
|
|
45
54
|
resolve,
|
|
46
55
|
startedAt,
|
|
@@ -52,6 +61,11 @@ export class DaemonChannel {
|
|
|
52
61
|
abortSignal: ctx.signal,
|
|
53
62
|
runId,
|
|
54
63
|
acc: { text: '', files: [], anomalies: [], toolCallCount: 0, seenToolCallIds: new Set() },
|
|
64
|
+
cleanupPolicy: triggerCleanupPolicy,
|
|
65
|
+
channelId: ctx.session.channelId,
|
|
66
|
+
agentAid: ctx.trigger.agentAid,
|
|
67
|
+
kind: `trigger:${ctx.trigger.id}`,
|
|
68
|
+
sessionId: ctx.session.id,
|
|
55
69
|
});
|
|
56
70
|
});
|
|
57
71
|
const message = {
|
|
@@ -85,8 +99,13 @@ export class DaemonChannel {
|
|
|
85
99
|
originChannelKey: ctx.trigger.origin?.channelKey,
|
|
86
100
|
originChannelType: ctx.trigger.origin?.channelType
|
|
87
101
|
?? (ctx.trigger.origin?.channelKey ? channelTypeFromKey(ctx.trigger.origin.channelKey) : undefined),
|
|
102
|
+
originChatType: ctx.trigger.origin?.delivery?.chatType,
|
|
103
|
+
originConversationId: ctx.trigger.origin?.delivery?.chatType === 'group'
|
|
104
|
+
? ctx.trigger.origin.channelId
|
|
105
|
+
: ctx.trigger.origin?.peerId,
|
|
88
106
|
originControl: ctx.trigger.origin?.channelKey === 'daemon',
|
|
89
107
|
authorizedBy: ctx.trigger.authorizedBy,
|
|
108
|
+
labels: ctx.trigger.labels,
|
|
90
109
|
},
|
|
91
110
|
};
|
|
92
111
|
const onAbort = () => {
|
|
@@ -128,6 +147,15 @@ export class DaemonChannel {
|
|
|
128
147
|
return promise;
|
|
129
148
|
}
|
|
130
149
|
async send(envelope, payload) {
|
|
150
|
+
// bg-session 路径:优先查 bgPending
|
|
151
|
+
const bgRunId = this.bgRunIdFromEnvelope(envelope);
|
|
152
|
+
if (bgRunId) {
|
|
153
|
+
const bgSlot = this.bgPending.get(bgRunId);
|
|
154
|
+
if (bgSlot && !bgSlot.terminated) {
|
|
155
|
+
this.handleBgSessionPayload(bgRunId, bgSlot, payload);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
131
159
|
const runId = this.runIdFromEnvelope(envelope);
|
|
132
160
|
if (!runId)
|
|
133
161
|
return;
|
|
@@ -270,6 +298,11 @@ export class DaemonChannel {
|
|
|
270
298
|
const pendingId = metadata?.triggerAttemptId ?? metadata?.triggerRunId;
|
|
271
299
|
return typeof pendingId === 'string' ? pendingId : undefined;
|
|
272
300
|
}
|
|
301
|
+
bgRunIdFromEnvelope(envelope) {
|
|
302
|
+
const metadata = envelope.replyContext?.metadata;
|
|
303
|
+
const runId = metadata?.bgSessionRunId;
|
|
304
|
+
return typeof runId === 'string' ? runId : undefined;
|
|
305
|
+
}
|
|
273
306
|
recordToolCall(slot, callId) {
|
|
274
307
|
if (typeof callId !== 'string' || !callId) {
|
|
275
308
|
slot.acc.toolCallCount += 1;
|
|
@@ -290,13 +323,14 @@ export class DaemonChannel {
|
|
|
290
323
|
?? (origin.channelKey ? channelTypeFromKey(origin.channelKey) : undefined);
|
|
291
324
|
if (!channelType)
|
|
292
325
|
return undefined;
|
|
326
|
+
const chatType = origin.delivery?.chatType ?? 'private';
|
|
293
327
|
return resolvePeerRoleDetail({
|
|
294
328
|
selfAid: trigger.agentAid,
|
|
295
329
|
channelKey: origin.channelKey,
|
|
296
330
|
channelType,
|
|
297
|
-
chatType
|
|
331
|
+
chatType,
|
|
298
332
|
actorId: origin.peerId,
|
|
299
|
-
conversationId: origin.peerId,
|
|
333
|
+
conversationId: chatType === 'group' ? origin.channelId : origin.peerId,
|
|
300
334
|
}).effectiveRole ?? undefined;
|
|
301
335
|
}
|
|
302
336
|
finish(runId, status, metadata) {
|
|
@@ -346,6 +380,29 @@ export class DaemonChannel {
|
|
|
346
380
|
toolCallCount: Math.max(slot.acc.toolCallCount, completedToolCallCount),
|
|
347
381
|
},
|
|
348
382
|
});
|
|
383
|
+
// trigger 清理策略
|
|
384
|
+
const policy = slot.cleanupPolicy;
|
|
385
|
+
if (policy.mode === 'immediate') {
|
|
386
|
+
void cleanupSessionRecord({ sessionId: slot.sessionId, channelId: slot.channelId, agentAid: slot.agentAid }, this.sessionManager);
|
|
387
|
+
}
|
|
388
|
+
else if (policy.mode === 'on_next_launch') {
|
|
389
|
+
recordNextLaunchCleanup(slot.kind, {
|
|
390
|
+
channelId: slot.channelId,
|
|
391
|
+
sessionId: slot.sessionId,
|
|
392
|
+
agentAid: slot.agentAid,
|
|
393
|
+
recordedAt: Date.now(),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else if (policy.mode === 'expire') {
|
|
397
|
+
appendExpireEntry({
|
|
398
|
+
expireAt: Date.now() + policy.ttlMs,
|
|
399
|
+
sessionId: slot.sessionId,
|
|
400
|
+
channelId: slot.channelId,
|
|
401
|
+
kind: slot.kind,
|
|
402
|
+
agentAid: slot.agentAid,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
// 'keep' 模式:不做任何处理
|
|
349
406
|
}
|
|
350
407
|
fail(runId, reason, text) {
|
|
351
408
|
const slot = this.pending.get(runId);
|
|
@@ -412,6 +469,204 @@ export class DaemonChannel {
|
|
|
412
469
|
}
|
|
413
470
|
queue.cancelActive?.(messageId);
|
|
414
471
|
}
|
|
472
|
+
// ── bg-session 生命周期 ──
|
|
473
|
+
handleBgSessionPayload(runId, slot, payload) {
|
|
474
|
+
if (slot.terminated)
|
|
475
|
+
return;
|
|
476
|
+
switch (payload.kind) {
|
|
477
|
+
case 'result.text':
|
|
478
|
+
if (payload.isFinal)
|
|
479
|
+
slot.text = payload.text;
|
|
480
|
+
else
|
|
481
|
+
slot.text += payload.text;
|
|
482
|
+
break;
|
|
483
|
+
case 'status.completed':
|
|
484
|
+
this.finishBgSession(runId, slot, 'completed');
|
|
485
|
+
break;
|
|
486
|
+
case 'result.error':
|
|
487
|
+
this.finishBgSession(runId, slot, 'error', payload.text ?? payload.reason ?? 'unknown error');
|
|
488
|
+
break;
|
|
489
|
+
case 'system.error':
|
|
490
|
+
this.finishBgSession(runId, slot, 'error', payload.text ?? payload.subtype ?? 'unknown error');
|
|
491
|
+
break;
|
|
492
|
+
case 'status.error':
|
|
493
|
+
this.finishBgSession(runId, slot, 'error', payload.metadata?.message ?? 'status error');
|
|
494
|
+
break;
|
|
495
|
+
case 'status.interrupted':
|
|
496
|
+
{
|
|
497
|
+
const reason = typeof payload.metadata?.reason === 'string'
|
|
498
|
+
? payload.metadata.reason
|
|
499
|
+
: 'interrupted';
|
|
500
|
+
const outcome = slot.abortSignal?.aborted ? 'cancelled' : 'interrupted';
|
|
501
|
+
this.finishBgSession(runId, slot, outcome, reason);
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
case 'status.timeout':
|
|
505
|
+
this.finishBgSession(runId, slot, 'error', 'bg-session watchdog timeout');
|
|
506
|
+
break;
|
|
507
|
+
default:
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
finishBgSession(runId, slot, outcome, error) {
|
|
512
|
+
if (slot.terminated)
|
|
513
|
+
return;
|
|
514
|
+
slot.terminated = true;
|
|
515
|
+
if (slot.watchdogTimer)
|
|
516
|
+
clearTimeout(slot.watchdogTimer);
|
|
517
|
+
if (slot.abortListener)
|
|
518
|
+
slot.abortSignal?.removeEventListener('abort', slot.abortListener);
|
|
519
|
+
this.bgPending.delete(runId);
|
|
520
|
+
// 会话结束后按清理策略处理
|
|
521
|
+
const policy = slot.cleanupPolicy;
|
|
522
|
+
if (policy.mode === 'immediate') {
|
|
523
|
+
void cleanupSessionRecord({ sessionId: slot.sessionId, channelId: slot.channelId, agentAid: slot.agentAid }, this.sessionManager);
|
|
524
|
+
}
|
|
525
|
+
else if (policy.mode === 'on_next_launch') {
|
|
526
|
+
recordNextLaunchCleanup(slot.kind, {
|
|
527
|
+
channelId: slot.channelId,
|
|
528
|
+
sessionId: slot.sessionId,
|
|
529
|
+
agentAid: slot.agentAid,
|
|
530
|
+
recordedAt: Date.now(),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
else if (policy.mode === 'expire') {
|
|
534
|
+
appendExpireEntry({
|
|
535
|
+
expireAt: Date.now() + policy.ttlMs,
|
|
536
|
+
sessionId: slot.sessionId,
|
|
537
|
+
channelId: slot.channelId,
|
|
538
|
+
kind: slot.kind,
|
|
539
|
+
agentAid: slot.agentAid,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// 'keep' 模式:不做任何处理
|
|
543
|
+
slot.resolve({
|
|
544
|
+
sessionId: slot.sessionId,
|
|
545
|
+
outcome,
|
|
546
|
+
...(slot.text ? { text: slot.text } : {}),
|
|
547
|
+
...(error ? { error } : {}),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* 拉起一个独立后台会话。会话创建、首条消息入队后即返回 handle;
|
|
552
|
+
* 会话结束由 handle.result 通知。实现 BackgroundSessionLauncher 接口。
|
|
553
|
+
* 详见 docs/background-session/background-session-launcher.md
|
|
554
|
+
*/
|
|
555
|
+
async launchBackgroundSession(spec) {
|
|
556
|
+
const runId = `bg-${spec.kind}-${spec.instanceId}`;
|
|
557
|
+
const channelId = `bg:${spec.kind}:${spec.instanceId}`;
|
|
558
|
+
const cleanupPolicy = spec.cleanupPolicy ?? { mode: 'on_next_launch' };
|
|
559
|
+
// 启动前清理:on_next_launch 清理同 kind 历史,expire 扫描到期条目
|
|
560
|
+
if (cleanupPolicy.mode === 'on_next_launch') {
|
|
561
|
+
await cleanupPreviousByKind(spec.kind, this.sessionManager);
|
|
562
|
+
}
|
|
563
|
+
else if (cleanupPolicy.mode !== 'keep') {
|
|
564
|
+
// immediate 和 expire 模式都在启动时跑一次 expire 扫描
|
|
565
|
+
await runExpireCleanup(this.sessionManager);
|
|
566
|
+
}
|
|
567
|
+
const session = await this.sessionManager.getOrCreateSession(this.channelName, channelId, spec.projectPath, channelId, // threadId,每次独立,保证 by_run 语义
|
|
568
|
+
{
|
|
569
|
+
channelKey: this.channelKey,
|
|
570
|
+
peerId: `bg:${spec.kind}`,
|
|
571
|
+
peerName: spec.kind,
|
|
572
|
+
peerType: 'system',
|
|
573
|
+
bgKind: spec.kind,
|
|
574
|
+
bgInstanceId: spec.instanceId,
|
|
575
|
+
...(spec.metadata ?? {}),
|
|
576
|
+
}, spec.kind, undefined, 'private', spec.baseagent, spec.agentAid, this.channelName, 'system');
|
|
577
|
+
const promise = new Promise((resolve) => {
|
|
578
|
+
const slot = {
|
|
579
|
+
resolve,
|
|
580
|
+
sessionId: session.id,
|
|
581
|
+
channelId,
|
|
582
|
+
kind: spec.kind,
|
|
583
|
+
startedAt: Date.now(),
|
|
584
|
+
abortSignal: spec.signal,
|
|
585
|
+
text: '',
|
|
586
|
+
terminated: false,
|
|
587
|
+
cleanupPolicy,
|
|
588
|
+
agentAid: spec.agentAid,
|
|
589
|
+
watchdogTimer: setTimeout(() => {
|
|
590
|
+
this.finishBgSession(runId, slot, 'error', 'bg-session watchdog timeout');
|
|
591
|
+
}, this.watchdogMs),
|
|
592
|
+
};
|
|
593
|
+
slot.watchdogTimer?.unref?.();
|
|
594
|
+
if (spec.signal) {
|
|
595
|
+
const onAbort = () => {
|
|
596
|
+
void this.cancelDelivery(runId).catch(() => { });
|
|
597
|
+
};
|
|
598
|
+
slot.abortListener = onAbort;
|
|
599
|
+
spec.signal.addEventListener('abort', onAbort, { once: true });
|
|
600
|
+
if (spec.signal.aborted)
|
|
601
|
+
onAbort();
|
|
602
|
+
}
|
|
603
|
+
this.bgPending.set(runId, slot);
|
|
604
|
+
});
|
|
605
|
+
const message = {
|
|
606
|
+
channel: this.channelName,
|
|
607
|
+
channelType: this.channelName,
|
|
608
|
+
channelId: session.channelId,
|
|
609
|
+
selfAID: spec.agentAid,
|
|
610
|
+
baseagent: spec.baseagent,
|
|
611
|
+
threadId: session.threadId || '',
|
|
612
|
+
chatType: 'private',
|
|
613
|
+
peerId: `bg:${spec.kind}`,
|
|
614
|
+
peerName: spec.kind,
|
|
615
|
+
peerType: 'system',
|
|
616
|
+
content: spec.prompt,
|
|
617
|
+
messageId: runId,
|
|
618
|
+
timestamp: Date.now(),
|
|
619
|
+
source: 'bg-session',
|
|
620
|
+
bgSessionMeta: {
|
|
621
|
+
kind: spec.kind,
|
|
622
|
+
runId,
|
|
623
|
+
instanceId: spec.instanceId,
|
|
624
|
+
boundSessionId: session.id,
|
|
625
|
+
},
|
|
626
|
+
replyContext: {
|
|
627
|
+
metadata: { bgSessionRunId: runId },
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
void this.messageQueue.enqueue?.(session.id, message, spec.projectPath, {
|
|
631
|
+
interruptible: false,
|
|
632
|
+
agentName: spec.agentAid,
|
|
633
|
+
sessionKeyField: session.sessionKey,
|
|
634
|
+
selfAID: spec.agentAid,
|
|
635
|
+
});
|
|
636
|
+
let settled = false;
|
|
637
|
+
void promise.then(() => { settled = true; });
|
|
638
|
+
const sessionId = session.id;
|
|
639
|
+
return {
|
|
640
|
+
sessionId,
|
|
641
|
+
result: promise,
|
|
642
|
+
waitForAgentSessionId: (opts) => this.waitForAgentSessionId(sessionId, () => settled, opts),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 轮询 session 记录,等 runner 写入 agentSessionId。
|
|
647
|
+
* 会话刚创建时只有 evolcore sessionId,agentSessionId 要等 runner 初始化并开始第一轮才写入。
|
|
648
|
+
* 会话结束后再读一次即停止,避免对已结束的会话空等到超时。
|
|
649
|
+
*/
|
|
650
|
+
async waitForAgentSessionId(sessionId, isSettled, opts = {}) {
|
|
651
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10_000);
|
|
652
|
+
const interval = opts.intervalMs ?? 250;
|
|
653
|
+
while (true) {
|
|
654
|
+
const settledBeforeRead = isSettled();
|
|
655
|
+
try {
|
|
656
|
+
const id = (await this.sessionManager.getSessionById(sessionId))?.agentSessionId;
|
|
657
|
+
if (id)
|
|
658
|
+
return id;
|
|
659
|
+
}
|
|
660
|
+
catch { /* 会话记录写到一半,下一轮再读 */ }
|
|
661
|
+
if (settledBeforeRead || Date.now() >= deadline)
|
|
662
|
+
return undefined;
|
|
663
|
+
await new Promise(r => setTimeout(r, interval));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** 实现 BackgroundSessionLauncher 接口 */
|
|
667
|
+
launch(spec) {
|
|
668
|
+
return this.launchBackgroundSession(spec);
|
|
669
|
+
}
|
|
415
670
|
}
|
|
416
671
|
function isObject(value) {
|
|
417
672
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|