evolcore 0.0.10 → 0.0.12
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 +44 -0
- package/README.md +3 -3
- package/bin/codex-managed-hook.mjs +80 -0
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +158 -44
- package/dist/agents/codex-app-server-client.js +153 -23
- package/dist/agents/codex-runner.js +331 -43
- package/dist/agents/ecagent-runner.js +53 -6
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/agents/request-identity.js +55 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/msg/group.js +81 -12
- package/dist/aun/msg/managed-operation.js +804 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/aun/outbox.js +28 -31
- package/dist/channels/aun.js +418 -235
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +72 -17
- package/dist/cli/agent.js +249 -22
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/daemon-commands.js +52 -33
- package/dist/cli/fs-command.js +60 -0
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +106 -63
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +11 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +4 -18
- package/dist/cli/trigger-command.js +4 -0
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/gateway-config.js +26 -10
- package/dist/config/mention-mode.js +8 -24
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/agent-delegation.js +0 -1
- package/dist/core/auth/operation-authorizer.js +62 -154
- package/dist/core/auth/operation-catalog.js +264 -22
- package/dist/core/bootstrap-messages.js +68 -0
- package/dist/core/bootstrap-service.js +144 -10
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +24 -7
- package/dist/core/command/agent-control.js +14 -11
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +467 -6
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +366 -0
- package/dist/core/command/menu-handler.js +213 -110
- package/dist/core/command/menu-protocol.js +71 -0
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +45 -25
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +12 -5
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +145 -21
- package/dist/core/handoff/store.js +24 -0
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +38 -4
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +237 -79
- package/dist/core/message/message-log.js +23 -0
- package/dist/core/message/message-queue.js +20 -4
- package/dist/core/message/response-engine.js +54 -36
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/ec-command-parser.js +156 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/sandbox-runtime.js +22 -1
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +292 -111
- package/dist/core/protected-paths.js +17 -9
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/core/session/session-manager.js +21 -2
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +644 -180
- package/dist/ipc.js +246 -19
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -0
- package/dist/trigger/script-executor.js +1 -0
- package/dist/utils/aid-bind.js +3 -8
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/msg.md +13 -0
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/rules/01-overview.md +9 -0
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +44 -0
- package/kits/templates/roles/visitor.json +40 -2
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +3 -2
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
- package/dist/core/command/evol-menu-version-gate.js +0 -39
package/dist/core/evolagent.js
CHANGED
|
@@ -52,7 +52,7 @@ export class EvolAgent {
|
|
|
52
52
|
const ba = this.baseagent;
|
|
53
53
|
// 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
|
|
54
54
|
try {
|
|
55
|
-
const effective = resolveEffective({ self: this.aid }, { cache: true });
|
|
55
|
+
const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
|
|
56
56
|
const block = effective.baseagents?.[ba];
|
|
57
57
|
return block?.model;
|
|
58
58
|
}
|
|
@@ -66,7 +66,7 @@ export class EvolAgent {
|
|
|
66
66
|
const ba = this.baseagent;
|
|
67
67
|
// 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
|
|
68
68
|
try {
|
|
69
|
-
const effective = resolveEffective({ self: this.aid }, { cache: true });
|
|
69
|
+
const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
|
|
70
70
|
const block = effective.baseagents?.[ba];
|
|
71
71
|
if (ba === 'codex')
|
|
72
72
|
return block?.effort ?? block?.reasoning;
|
|
@@ -257,7 +257,8 @@ export class EvolAgent {
|
|
|
257
257
|
this.persist();
|
|
258
258
|
}
|
|
259
259
|
setLifecycle(value) {
|
|
260
|
-
|
|
260
|
+
const current = cfgRead(ConfigTarget.Agent, { self: this.aid });
|
|
261
|
+
this.rawAgent = withLifecycleForWrite(current || this.rawAgent, value);
|
|
261
262
|
this.rawAgent.$schema_version = Math.max(this.rawAgent.$schema_version || 0, this.merged.$schema_version || 0);
|
|
262
263
|
this.merged.lifecycle = value;
|
|
263
264
|
delete this.merged.initialized;
|
|
@@ -275,7 +276,8 @@ export class EvolAgent {
|
|
|
275
276
|
*/
|
|
276
277
|
getPersona() {
|
|
277
278
|
const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
|
|
278
|
-
|
|
279
|
+
const raw = fileCache.getText(personaPath, { policy: 'mtime', group: this.agentFilesGroup() });
|
|
280
|
+
return raw === null ? null : (raw.trim() || null);
|
|
279
281
|
}
|
|
280
282
|
/**
|
|
281
283
|
* 读取 personal/memory/working.md 内容。走 fileCache(mtime 门控):
|
|
@@ -284,7 +286,8 @@ export class EvolAgent {
|
|
|
284
286
|
*/
|
|
285
287
|
getWorkingMemory() {
|
|
286
288
|
const workingPath = path.join(agentPersonalDir(this.aid), 'memory', 'working.md');
|
|
287
|
-
|
|
289
|
+
const raw = fileCache.getText(workingPath, { policy: 'mtime', group: this.agentFilesGroup() });
|
|
290
|
+
return raw === null ? null : (raw.trim() || null);
|
|
288
291
|
}
|
|
289
292
|
/** 清除本 agent 身份层缓存(reload 后重新读取)。只失效自己的文件组,不波及他人。 */
|
|
290
293
|
invalidatePersonaCache() {
|
|
@@ -318,6 +321,10 @@ export class EvolAgent {
|
|
|
318
321
|
this.rawAgent = rawAgent;
|
|
319
322
|
this.merged = merged;
|
|
320
323
|
}
|
|
324
|
+
/** Reload uses the current object references as an in-memory rollback snapshot. */
|
|
325
|
+
captureConfigSnapshot() {
|
|
326
|
+
return { rawAgent: this.rawAgent, merged: this.merged };
|
|
327
|
+
}
|
|
321
328
|
// ── 内部辅助 ─────────────────────────────────────────────────────────
|
|
322
329
|
/**
|
|
323
330
|
* 找 rawAgent.channels 里的可变实例,用于写入。
|
|
@@ -117,8 +117,9 @@ export class HandoffDispatcher {
|
|
|
117
117
|
if (this.pausedAgents.has(selfAid))
|
|
118
118
|
return false;
|
|
119
119
|
const current = this.store.get(selfAid, next.handoff_id);
|
|
120
|
-
if (!current || current.state !== 'queued' || current.attention_required)
|
|
121
|
-
return current?.state === 'target_sent';
|
|
120
|
+
if (!current || current.state !== 'queued' || current.attention_required) {
|
|
121
|
+
return current?.state === 'target_sent' || current?.state === 'discarded';
|
|
122
|
+
}
|
|
122
123
|
this.store.recordSendStarted(selfAid, current.handoff_id);
|
|
123
124
|
try {
|
|
124
125
|
const send = await this.sendWithinQueueDeadline(current);
|
|
@@ -171,6 +172,8 @@ export class HandoffDispatcher {
|
|
|
171
172
|
await new Promise(resolve => setTimeout(resolve, this.retryDelaysMs[Math.min(attempt - 1, this.retryDelaysMs.length - 1)] ?? 0));
|
|
172
173
|
}
|
|
173
174
|
}
|
|
175
|
+
if (this.store.get(selfAid, next.handoff_id)?.state === 'discarded')
|
|
176
|
+
continue;
|
|
174
177
|
if (!sent) {
|
|
175
178
|
this.store.recordSendExhausted(selfAid, next.handoff_id);
|
|
176
179
|
logger.error(`[Handoff] target send retries exhausted: ${next.handoff_id}`);
|
|
@@ -7,6 +7,7 @@ import { createRootCausation, deriveCausation, normalizeCausation } from '../cau
|
|
|
7
7
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
9
|
const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
|
|
10
|
+
const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
11
|
const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
|
|
11
12
|
const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
|
|
12
13
|
export class HandoffTargetBlockedError extends Error {
|
|
@@ -43,6 +44,15 @@ export class TriggerHandoffRequiredForbiddenError extends Error {
|
|
|
43
44
|
this.name = 'TriggerHandoffRequiredForbiddenError';
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
export class HandoffSessionReplacedError extends Error {
|
|
48
|
+
sessionId;
|
|
49
|
+
code = 'HANDOFF_SESSION_REPLACED';
|
|
50
|
+
constructor(sessionId) {
|
|
51
|
+
super(`handoff session was replaced during creation: ${sessionId}`);
|
|
52
|
+
this.sessionId = sessionId;
|
|
53
|
+
this.name = 'HandoffSessionReplacedError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
46
56
|
export class HandoffRuntime {
|
|
47
57
|
sessionManager;
|
|
48
58
|
messageQueue;
|
|
@@ -52,6 +62,8 @@ export class HandoffRuntime {
|
|
|
52
62
|
now;
|
|
53
63
|
staleWarned = new Set();
|
|
54
64
|
largeAggregateWarned = new Set();
|
|
65
|
+
lifecycleSequence = 0;
|
|
66
|
+
sessionDiscardSequence = new Map();
|
|
55
67
|
constructor(sessionManager, messageQueue, sender, store = new HandoffStore(), options = {}) {
|
|
56
68
|
this.sessionManager = sessionManager;
|
|
57
69
|
this.messageQueue = messageQueue;
|
|
@@ -60,6 +72,7 @@ export class HandoffRuntime {
|
|
|
60
72
|
this.dispatcher = new HandoffDispatcher(store, this.mutexes, sender, options);
|
|
61
73
|
}
|
|
62
74
|
async createOutbound(input) {
|
|
75
|
+
const operationSequence = ++this.lifecycleSequence;
|
|
63
76
|
throwIfAborted(input.signal);
|
|
64
77
|
const origin = await this.sessionManager.getSessionById(input.originSessionId);
|
|
65
78
|
throwIfAborted(input.signal);
|
|
@@ -81,24 +94,29 @@ export class HandoffRuntime {
|
|
|
81
94
|
if (input.explicitReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
|
|
82
95
|
throw new TriggerHandoffRequiredForbiddenError();
|
|
83
96
|
}
|
|
84
|
-
await this.dispatcher.expireTargetQueued(input.selfAid, target.id);
|
|
85
|
-
throwIfAborted(input.signal);
|
|
86
|
-
const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
|
|
87
|
-
if (head?.attention_required) {
|
|
88
|
-
throw new HandoffTargetBlockedError(head.handoff_id, head.attention_reason ?? 'UNKNOWN_ATTENTION');
|
|
89
|
-
}
|
|
90
97
|
const handoffCausation = deriveCausation(normalizeCausation(input.causation) ?? createRootCausation());
|
|
91
|
-
const handoff = this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const handoff = await this.mutexes.forKey(this.targetKey(input.selfAid, target.id)).runExclusive(async () => {
|
|
99
|
+
const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
|
|
100
|
+
if (replacedSessionId)
|
|
101
|
+
throw new HandoffSessionReplacedError(replacedSessionId);
|
|
102
|
+
this.dispatcher.expireQueued(input.selfAid, target.id);
|
|
103
|
+
throwIfAborted(input.signal);
|
|
104
|
+
const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
|
|
105
|
+
if (head?.attention_required) {
|
|
106
|
+
throw new HandoffTargetBlockedError(head.handoff_id, head.attention_reason ?? 'UNKNOWN_ATTENTION');
|
|
107
|
+
}
|
|
108
|
+
return this.store.create({
|
|
109
|
+
selfAid: input.selfAid,
|
|
110
|
+
originSessionId: origin.id,
|
|
111
|
+
originMessageId: input.originMessageId,
|
|
112
|
+
targetSessionId: target.id,
|
|
113
|
+
payload: input.payload,
|
|
114
|
+
encrypt: input.encrypt,
|
|
115
|
+
returnPolicy: input.explicitReturnPolicy ?? 'required',
|
|
116
|
+
originAuthorization: input.originAuthorization,
|
|
117
|
+
causation: handoffCausation,
|
|
118
|
+
now: this.now?.(),
|
|
119
|
+
});
|
|
102
120
|
});
|
|
103
121
|
recordCausationSpan(handoffCausation, 'handoff.request', {
|
|
104
122
|
status: 'started',
|
|
@@ -106,6 +124,10 @@ export class HandoffRuntime {
|
|
|
106
124
|
});
|
|
107
125
|
await this.dispatcher.drain(input.selfAid, target.id);
|
|
108
126
|
const dispatched = this.store.get(input.selfAid, handoff.handoff_id) ?? handoff;
|
|
127
|
+
const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
|
|
128
|
+
if (dispatched.state === 'discarded' || replacedSessionId) {
|
|
129
|
+
throw new HandoffSessionReplacedError(replacedSessionId ?? target.id);
|
|
130
|
+
}
|
|
109
131
|
if (dispatched.state === 'failed') {
|
|
110
132
|
throw new HandoffDeliveryFailedError(dispatched.handoff_id);
|
|
111
133
|
}
|
|
@@ -142,7 +164,9 @@ export class HandoffRuntime {
|
|
|
142
164
|
}
|
|
143
165
|
markBindingIncomplete(selfAid, handoffId) {
|
|
144
166
|
const instance = this.store.get(selfAid, handoffId);
|
|
145
|
-
if (instance
|
|
167
|
+
if (instance
|
|
168
|
+
&& (instance.state === 'target_context_pending' || instance.state === 'return_pending')
|
|
169
|
+
&& !instance.attention_required) {
|
|
146
170
|
this.store.markAttention(selfAid, handoffId, 'REPLY_BINDING_INCOMPLETE');
|
|
147
171
|
}
|
|
148
172
|
}
|
|
@@ -207,16 +231,31 @@ export class HandoffRuntime {
|
|
|
207
231
|
role: origin.chatType === 'group' ? (origin.identity?.role ?? 'none') : undefined,
|
|
208
232
|
sessionKeyField: origin.sessionKey,
|
|
209
233
|
selfAID: origin.selfAID,
|
|
210
|
-
onPersisted: () =>
|
|
234
|
+
onPersisted: () => {
|
|
235
|
+
if (this.store.get(selfAid, handoffId)?.state !== 'origin_queued') {
|
|
236
|
+
throw new Error(`handoff is no longer deliverable: ${handoffId}`);
|
|
237
|
+
}
|
|
238
|
+
this.store.recordOriginEnqueued(selfAid, handoffId, messageId);
|
|
239
|
+
},
|
|
211
240
|
});
|
|
212
241
|
}
|
|
213
242
|
catch (error) {
|
|
214
|
-
this.store.
|
|
243
|
+
if (this.store.get(selfAid, handoffId)?.state === 'origin_queued') {
|
|
244
|
+
this.store.recordOriginEnqueueFailed(selfAid, handoffId, error instanceof Error ? error.message : String(error));
|
|
245
|
+
}
|
|
215
246
|
}
|
|
216
247
|
}
|
|
217
248
|
buildPromptItem(message) {
|
|
218
249
|
return this.buildPromptItems(message)[0] ?? null;
|
|
219
250
|
}
|
|
251
|
+
isDiscardedDelivery(message) {
|
|
252
|
+
const delivery = message.handoffDelivery;
|
|
253
|
+
const selfAid = message.selfAID;
|
|
254
|
+
if (!delivery || !selfAid)
|
|
255
|
+
return false;
|
|
256
|
+
const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
|
|
257
|
+
return ids.length > 0 && ids.every(handoffId => this.store.get(selfAid, handoffId)?.state === 'discarded');
|
|
258
|
+
}
|
|
220
259
|
buildPromptItems(message) {
|
|
221
260
|
const delivery = message.handoffDelivery;
|
|
222
261
|
const selfAid = message.selfAID;
|
|
@@ -367,9 +406,54 @@ export class HandoffRuntime {
|
|
|
367
406
|
expired += this.dispatcher.expireQueued(selfAid).length;
|
|
368
407
|
return expired;
|
|
369
408
|
}
|
|
370
|
-
async
|
|
409
|
+
async discardSession(selfAid, sessionId) {
|
|
410
|
+
this.sessionDiscardSequence.set(this.sessionKey(selfAid, sessionId), ++this.lifecycleSequence);
|
|
411
|
+
const candidates = this.store.list(selfAid)
|
|
412
|
+
.filter(instance => (instance.origin_session_id === sessionId || instance.target_session_id === sessionId) && instance.state !== 'failed' && instance.state !== 'completed' && instance.state !== 'discarded');
|
|
413
|
+
let discarded = 0;
|
|
414
|
+
for (const instance of candidates) {
|
|
415
|
+
const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'session_replaced', {
|
|
416
|
+
session_id: sessionId,
|
|
417
|
+
});
|
|
418
|
+
if (result)
|
|
419
|
+
discarded++;
|
|
420
|
+
}
|
|
421
|
+
return discarded;
|
|
422
|
+
}
|
|
423
|
+
async discardExpired(selfAids, retentionMs = HANDOFF_RETENTION_MS) {
|
|
424
|
+
const now = this.now?.() ?? Date.now();
|
|
425
|
+
let discarded = 0;
|
|
426
|
+
for (const selfAid of selfAids) {
|
|
427
|
+
const latestEventVersions = new Map();
|
|
428
|
+
for (const event of this.store.events(selfAid)) {
|
|
429
|
+
if (!event.handoff_id || event.to_version === undefined)
|
|
430
|
+
continue;
|
|
431
|
+
latestEventVersions.set(event.handoff_id, Math.max(latestEventVersions.get(event.handoff_id) ?? 0, event.to_version));
|
|
432
|
+
}
|
|
433
|
+
for (const instance of this.store.list(selfAid)) {
|
|
434
|
+
if (instance.state === 'failed' || instance.state === 'completed' || instance.state === 'discarded')
|
|
435
|
+
continue;
|
|
436
|
+
if (now - instance.created_at <= retentionMs)
|
|
437
|
+
continue;
|
|
438
|
+
if ((latestEventVersions.get(instance.handoff_id) ?? 0) !== instance.version)
|
|
439
|
+
continue;
|
|
440
|
+
const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'retention_expired', {
|
|
441
|
+
retention_ms: retentionMs,
|
|
442
|
+
age_ms: now - instance.created_at,
|
|
443
|
+
}, instance.version, now);
|
|
444
|
+
if (result) {
|
|
445
|
+
discarded++;
|
|
446
|
+
logger.info(`[Handoff] discarded expired handoff: ${instance.handoff_id}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return discarded;
|
|
451
|
+
}
|
|
452
|
+
async recover(selfAids, options = {}) {
|
|
371
453
|
const originEnqueues = [];
|
|
372
454
|
for (const selfAid of selfAids) {
|
|
455
|
+
if (options.discardExpired !== false)
|
|
456
|
+
await this.discardExpired([selfAid]);
|
|
373
457
|
// v1 initially represented return=none as return_pending and required an
|
|
374
458
|
// empty manual return. It has already been shown to a target turn, so
|
|
375
459
|
// finish it before version reconciliation instead of prompting again.
|
|
@@ -412,6 +496,40 @@ export class HandoffRuntime {
|
|
|
412
496
|
}
|
|
413
497
|
await Promise.all(originEnqueues);
|
|
414
498
|
}
|
|
499
|
+
async discardWithTargetLock(selfAid, handoffId, reason, details, expectedVersion, now = this.now?.()) {
|
|
500
|
+
const snapshot = this.store.get(selfAid, handoffId);
|
|
501
|
+
if (!snapshot)
|
|
502
|
+
return false;
|
|
503
|
+
const discarded = await this.mutexes.forKey(this.targetKey(selfAid, snapshot.target_session_id)).runExclusive(async () => {
|
|
504
|
+
const current = this.store.get(selfAid, handoffId);
|
|
505
|
+
if (!current || current.state === 'failed' || current.state === 'completed' || current.state === 'discarded') {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
if (expectedVersion !== undefined && current.version !== expectedVersion)
|
|
509
|
+
return null;
|
|
510
|
+
this.store.discard(selfAid, handoffId, reason, details, now);
|
|
511
|
+
return current;
|
|
512
|
+
});
|
|
513
|
+
if (!discarded)
|
|
514
|
+
return false;
|
|
515
|
+
await this.cancelOriginDelivery(discarded);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
async cancelOriginDelivery(instance) {
|
|
519
|
+
if (instance.state !== 'origin_queued' && instance.state !== 'origin_delivered')
|
|
520
|
+
return;
|
|
521
|
+
const messageId = instance.origin_delivery_message_id
|
|
522
|
+
?? `origin-deliver:${instance.handoff_id}:${instance.origin_session_id}`;
|
|
523
|
+
const queue = this.messageQueue;
|
|
524
|
+
try {
|
|
525
|
+
const queued = queue.cancelMessageByIdInSession?.(instance.origin_session_id, messageId) ?? false;
|
|
526
|
+
if (!queued)
|
|
527
|
+
await queue.cancelActiveAndWait?.(messageId);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
logger.warn(`[Handoff] failed to cancel discarded origin delivery ${messageId}:`, error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
415
533
|
warnIfStale(instance) {
|
|
416
534
|
if (instance.state !== 'target_sent' && instance.state !== 'return_pending')
|
|
417
535
|
return;
|
|
@@ -425,6 +543,12 @@ export class HandoffRuntime {
|
|
|
425
543
|
+ ` ageHours=${Math.floor(ageMs / 3_600_000)}`
|
|
426
544
|
+ ` originSession=${instance.origin_session_id} targetSession=${instance.target_session_id}`);
|
|
427
545
|
}
|
|
546
|
+
targetKey(selfAid, targetSessionId) {
|
|
547
|
+
return `${selfAid}\u0000${targetSessionId}`;
|
|
548
|
+
}
|
|
549
|
+
sessionKey(selfAid, sessionId) {
|
|
550
|
+
return `${selfAid}\u0000${sessionId}`;
|
|
551
|
+
}
|
|
428
552
|
warnIfLargeAggregate(message, instances, direction) {
|
|
429
553
|
const chars = message.content.length
|
|
430
554
|
+ instances.reduce((total, instance) => total + this.requestSummary(instance).length, 0);
|
|
@@ -9,6 +9,7 @@ import { HANDOFF_SCHEMA_VERSION, HANDOFF_QUERY_DEFAULT_LIMIT, HANDOFF_QUERY_MAX_
|
|
|
9
9
|
import { normalizeCausation } from '../causation/context.js';
|
|
10
10
|
const SAFE_ID_RE = /^[A-Za-z0-9._-]+$/;
|
|
11
11
|
export const HANDOFF_ID_RE = /^h-[A-Za-z0-9._-]+$/;
|
|
12
|
+
const TERMINAL_HANDOFF_STATES = new Set(['failed', 'completed', 'discarded']);
|
|
12
13
|
function safeSegment(value, field) {
|
|
13
14
|
if (!value || !SAFE_ID_RE.test(value) || value === '.' || value === '..') {
|
|
14
15
|
throw new Error(`invalid ${field}`);
|
|
@@ -514,6 +515,29 @@ export class HandoffStore {
|
|
|
514
515
|
this.write(selfAid, instance);
|
|
515
516
|
return instance;
|
|
516
517
|
}
|
|
518
|
+
discard(selfAid, handoffId, reason, details = {}, now = Date.now()) {
|
|
519
|
+
const instance = this.get(selfAid, handoffId);
|
|
520
|
+
if (!instance)
|
|
521
|
+
throw new Error('handoff not found');
|
|
522
|
+
if (TERMINAL_HANDOFF_STATES.has(instance.state))
|
|
523
|
+
return instance;
|
|
524
|
+
const previousState = instance.state;
|
|
525
|
+
const previousVersion = instance.version;
|
|
526
|
+
instance.state = 'discarded';
|
|
527
|
+
instance.attention_required = false;
|
|
528
|
+
instance.attention_reason = null;
|
|
529
|
+
instance.version++;
|
|
530
|
+
instance.updated_at = now;
|
|
531
|
+
instance.completed_at = now;
|
|
532
|
+
this.appendEvent(selfAid, 'handoff_discarded', handoffId, {
|
|
533
|
+
fromVersion: previousVersion,
|
|
534
|
+
toVersion: instance.version,
|
|
535
|
+
details: { ...details, reason, previous_state: previousState },
|
|
536
|
+
now,
|
|
537
|
+
});
|
|
538
|
+
this.write(selfAid, instance);
|
|
539
|
+
return instance;
|
|
540
|
+
}
|
|
517
541
|
requireState(selfAid, handoffId, expected) {
|
|
518
542
|
const instance = this.get(selfAid, handoffId);
|
|
519
543
|
if (!instance)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveEcagentConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
|
|
2
|
+
import { buildModelRequestHeaders } from '../../agents/request-identity.js';
|
|
2
3
|
function apiEndpoint(baseUrl, resource) {
|
|
3
4
|
const configured = baseUrl?.trim();
|
|
4
5
|
if (!configured)
|
|
@@ -119,12 +120,25 @@ export class EcagentTextInferenceProvider {
|
|
|
119
120
|
baseagent = 'ecagent';
|
|
120
121
|
constructor(config) {
|
|
121
122
|
this.config = config;
|
|
123
|
+
this.config = {
|
|
124
|
+
...config,
|
|
125
|
+
headers: buildModelRequestHeaders({
|
|
126
|
+
baseagent: 'ecagent',
|
|
127
|
+
baseUrl: config.baseUrl,
|
|
128
|
+
agentAid: config.evolcoreAgentAid,
|
|
129
|
+
configuredHeaders: config.headers,
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
122
132
|
}
|
|
123
133
|
async completeText(request) {
|
|
124
134
|
const response = await fetch(apiEndpoint(this.config.baseUrl, 'chat/completions'), {
|
|
125
135
|
method: 'POST',
|
|
126
136
|
signal: request.signal,
|
|
127
|
-
headers: {
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
140
|
+
...this.config.headers,
|
|
141
|
+
},
|
|
128
142
|
body: JSON.stringify({
|
|
129
143
|
model: request.model,
|
|
130
144
|
messages: [{ role: 'system', content: request.system }, { role: 'user', content: request.input }],
|
|
@@ -150,15 +164,35 @@ export function createTextInferenceProvider(baseagent, config) {
|
|
|
150
164
|
const canonical = normalizeBaseagent(baseagent).canonical;
|
|
151
165
|
if (canonical === 'claude') {
|
|
152
166
|
const resolved = resolveAnthropicDirectConfig(config.baseagents?.claude);
|
|
153
|
-
return resolved ? new AnthropicTextInferenceProvider(
|
|
167
|
+
return resolved ? new AnthropicTextInferenceProvider({
|
|
168
|
+
...resolved,
|
|
169
|
+
headers: buildModelRequestHeaders({
|
|
170
|
+
baseagent: 'claude',
|
|
171
|
+
baseUrl: resolved.baseUrl,
|
|
172
|
+
agentAid: config.aid,
|
|
173
|
+
configuredHeaders: resolved.headers,
|
|
174
|
+
}),
|
|
175
|
+
}) : undefined;
|
|
154
176
|
}
|
|
155
177
|
if (canonical === 'codex') {
|
|
156
178
|
const resolved = resolveOpenaiDirectConfig(config.baseagents?.codex);
|
|
157
|
-
return resolved ? new OpenAITextInferenceProvider(
|
|
179
|
+
return resolved ? new OpenAITextInferenceProvider({
|
|
180
|
+
...resolved,
|
|
181
|
+
headers: buildModelRequestHeaders({
|
|
182
|
+
baseagent: 'codex',
|
|
183
|
+
baseUrl: resolved.baseUrl,
|
|
184
|
+
agentAid: config.aid,
|
|
185
|
+
configuredHeaders: resolved.headers,
|
|
186
|
+
}),
|
|
187
|
+
}) : undefined;
|
|
158
188
|
}
|
|
159
189
|
if (canonical === 'ecagent') {
|
|
160
190
|
try {
|
|
161
|
-
const
|
|
191
|
+
const override = {
|
|
192
|
+
...(config.baseagents?.ecagent ?? {}),
|
|
193
|
+
evolcoreAgentAid: config.aid,
|
|
194
|
+
};
|
|
195
|
+
const resolved = resolveEcagentConfig({ agents: { ecagent: override } }, override);
|
|
162
196
|
return new EcagentTextInferenceProvider(resolved);
|
|
163
197
|
}
|
|
164
198
|
catch {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export class MentionSchemaError extends Error {
|
|
2
|
+
code = 'INVALID_MENTION_SCHEMA';
|
|
3
|
+
index;
|
|
4
|
+
constructor(message, index) {
|
|
5
|
+
super(index === undefined ? message : `mentions[${index}]: ${message}`);
|
|
6
|
+
this.name = 'MentionSchemaError';
|
|
7
|
+
this.index = index;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function hasOwn(value, key) {
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
15
|
+
}
|
|
16
|
+
function assertMetadata(value, index) {
|
|
17
|
+
if (hasOwn(value, 'display') && typeof value.display !== 'string') {
|
|
18
|
+
throw new MentionSchemaError('display must be a string', index);
|
|
19
|
+
}
|
|
20
|
+
for (const key of ['offset', 'length']) {
|
|
21
|
+
if (!hasOwn(value, key))
|
|
22
|
+
continue;
|
|
23
|
+
const field = value[key];
|
|
24
|
+
if (typeof field !== 'number' || !Number.isInteger(field) || field < 0) {
|
|
25
|
+
throw new MentionSchemaError(`${key} must be a non-negative integer`, index);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const key of ['name', 'key']) {
|
|
29
|
+
if (hasOwn(value, key) && typeof value[key] !== 'string') {
|
|
30
|
+
throw new MentionSchemaError(`${key} must be a string`, index);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate and copy canonical mention entries.
|
|
36
|
+
*
|
|
37
|
+
* This is intentionally strict: the old string array format is rejected
|
|
38
|
+
* instead of being converted, because conversion can hide a producer bug and
|
|
39
|
+
* make routing appear to work with the wrong wire contract.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeMentionEntries(raw) {
|
|
42
|
+
if (raw === undefined)
|
|
43
|
+
return [];
|
|
44
|
+
if (!Array.isArray(raw)) {
|
|
45
|
+
throw new MentionSchemaError('mentions must be an array');
|
|
46
|
+
}
|
|
47
|
+
const normalized = [];
|
|
48
|
+
for (const [index, value] of raw.entries()) {
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
throw new MentionSchemaError('legacy string entries are not supported; use {aid} or {scope:"all"}', index);
|
|
51
|
+
}
|
|
52
|
+
if (!isRecord(value)) {
|
|
53
|
+
throw new MentionSchemaError('entry must be an object', index);
|
|
54
|
+
}
|
|
55
|
+
const hasScope = hasOwn(value, 'scope');
|
|
56
|
+
const hasAid = hasOwn(value, 'aid');
|
|
57
|
+
const hasUserId = hasOwn(value, 'userId');
|
|
58
|
+
const discriminatorCount = Number(hasScope) + Number(hasAid) + Number(hasUserId);
|
|
59
|
+
if (discriminatorCount !== 1) {
|
|
60
|
+
throw new MentionSchemaError('entry must contain exactly one of scope, aid, or userId', index);
|
|
61
|
+
}
|
|
62
|
+
assertMetadata(value, index);
|
|
63
|
+
if (hasScope) {
|
|
64
|
+
if (value.scope !== 'all') {
|
|
65
|
+
throw new MentionSchemaError('scope must be "all"', index);
|
|
66
|
+
}
|
|
67
|
+
normalized.push({ ...value, scope: 'all' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (hasAid) {
|
|
71
|
+
if (typeof value.aid !== 'string' || value.aid.length === 0 || value.aid.trim() !== value.aid) {
|
|
72
|
+
throw new MentionSchemaError('aid must be a non-empty trimmed string', index);
|
|
73
|
+
}
|
|
74
|
+
if (value.aid === 'all') {
|
|
75
|
+
throw new MentionSchemaError('all is a scope, not an aid; use {scope:"all"}', index);
|
|
76
|
+
}
|
|
77
|
+
normalized.push({ ...value, aid: value.aid });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value.userId !== 'string' || value.userId.length === 0 || value.userId.trim() !== value.userId) {
|
|
81
|
+
throw new MentionSchemaError('userId must be a non-empty trimmed string', index);
|
|
82
|
+
}
|
|
83
|
+
if (value.userId === 'all') {
|
|
84
|
+
throw new MentionSchemaError('all is a scope, not a userId; use {scope:"all"}', index);
|
|
85
|
+
}
|
|
86
|
+
normalized.push({ ...value, userId: value.userId });
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
export function mentionEntryIsAll(entry) {
|
|
91
|
+
return 'scope' in entry && entry.scope === 'all';
|
|
92
|
+
}
|
|
93
|
+
/** Return all concrete target IDs, including channel-native userId entries. */
|
|
94
|
+
export function mentionEntryTargets(entries) {
|
|
95
|
+
const targets = [];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const target = 'aid' in entry && typeof entry.aid === 'string'
|
|
99
|
+
? entry.aid
|
|
100
|
+
: 'userId' in entry && typeof entry.userId === 'string'
|
|
101
|
+
? entry.userId
|
|
102
|
+
: undefined;
|
|
103
|
+
if (!target || target === 'all' || seen.has(target))
|
|
104
|
+
continue;
|
|
105
|
+
seen.add(target);
|
|
106
|
+
targets.push(target);
|
|
107
|
+
}
|
|
108
|
+
return targets;
|
|
109
|
+
}
|
|
110
|
+
export function hasMentionAll(entries) {
|
|
111
|
+
return entries.some(mentionEntryIsAll);
|
|
112
|
+
}
|
|
113
|
+
/** IDs used by renderers; keeps `all` visible as a derived value. */
|
|
114
|
+
export function mentionEntryAids(entries) {
|
|
115
|
+
const aids = mentionEntryTargets(entries);
|
|
116
|
+
if (hasMentionAll(entries))
|
|
117
|
+
aids.push('all');
|
|
118
|
+
return aids;
|
|
119
|
+
}
|
|
120
|
+
export function mentionEntryKey(entry) {
|
|
121
|
+
if ('scope' in entry && entry.scope === 'all')
|
|
122
|
+
return 'scope:all';
|
|
123
|
+
if ('aid' in entry)
|
|
124
|
+
return `aid:${entry.aid}`;
|
|
125
|
+
return `user:${entry.userId}`;
|
|
126
|
+
}
|