evolcore 0.0.11 → 0.0.13
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/bin/codex-managed-hook.mjs +80 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +86 -7
- package/dist/agents/codex-app-server-client.js +174 -20
- package/dist/agents/codex-runner.js +375 -92
- package/dist/agents/ecagent-runner.js +39 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +91 -44
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +865 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +655 -240
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +108 -31
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +70 -55
- package/dist/cli/fs-command.js +79 -10
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/init.js +77 -8
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +53 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +7 -32
- package/dist/cli/trigger-command.js +11 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +9 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -2
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +31 -7
- package/dist/core/auth/operation-catalog.js +196 -25
- package/dist/core/bootstrap-messages.js +25 -0
- package/dist/core/bootstrap-service.js +64 -4
- 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 +1 -1
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +650 -42
- 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 +370 -0
- package/dist/core/command/menu-handler.js +230 -51
- package/dist/core/command/menu-protocol.js +94 -2
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +99 -31
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +157 -22
- package/dist/core/handoff/store.js +29 -1
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- 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 +298 -88
- package/dist/core/message/message-log.js +3 -0
- package/dist/core/message/message-queue.js +268 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +161 -77
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +243 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +43 -1
- package/dist/core/permission/tool-policy.js +322 -57
- package/dist/core/protected-paths.js +114 -4
- package/dist/core/session/session-manager.js +42 -4
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +541 -180
- package/dist/ipc.js +232 -10
- package/dist/paths.js +3 -0
- 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/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/script-executor.js +1 -0
- package/dist/trigger/validation.js +67 -6
- 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/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- 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/trigger.md +17 -1
- 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/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- 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 +34 -1
- package/kits/templates/roles/visitor.json +35 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/command/evol-menu-version-gate.js +0 -39
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -6,7 +6,9 @@ import { HandoffStore } from './store.js';
|
|
|
6
6
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
7
7
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
|
+
import { replyContextFromSession } from '../message/message-utils.js';
|
|
9
10
|
const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
|
|
11
|
+
const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
12
|
const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
|
|
11
13
|
const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
|
|
12
14
|
export class HandoffTargetBlockedError extends Error {
|
|
@@ -43,6 +45,15 @@ export class TriggerHandoffRequiredForbiddenError extends Error {
|
|
|
43
45
|
this.name = 'TriggerHandoffRequiredForbiddenError';
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
export class HandoffSessionReplacedError extends Error {
|
|
49
|
+
sessionId;
|
|
50
|
+
code = 'HANDOFF_SESSION_REPLACED';
|
|
51
|
+
constructor(sessionId) {
|
|
52
|
+
super(`handoff session was replaced during creation: ${sessionId}`);
|
|
53
|
+
this.sessionId = sessionId;
|
|
54
|
+
this.name = 'HandoffSessionReplacedError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
46
57
|
export class HandoffRuntime {
|
|
47
58
|
sessionManager;
|
|
48
59
|
messageQueue;
|
|
@@ -52,6 +63,8 @@ export class HandoffRuntime {
|
|
|
52
63
|
now;
|
|
53
64
|
staleWarned = new Set();
|
|
54
65
|
largeAggregateWarned = new Set();
|
|
66
|
+
lifecycleSequence = 0;
|
|
67
|
+
sessionDiscardSequence = new Map();
|
|
55
68
|
constructor(sessionManager, messageQueue, sender, store = new HandoffStore(), options = {}) {
|
|
56
69
|
this.sessionManager = sessionManager;
|
|
57
70
|
this.messageQueue = messageQueue;
|
|
@@ -60,13 +73,19 @@ export class HandoffRuntime {
|
|
|
60
73
|
this.dispatcher = new HandoffDispatcher(store, this.mutexes, sender, options);
|
|
61
74
|
}
|
|
62
75
|
async createOutbound(input) {
|
|
76
|
+
const operationSequence = ++this.lifecycleSequence;
|
|
63
77
|
throwIfAborted(input.signal);
|
|
64
78
|
const origin = await this.sessionManager.getSessionById(input.originSessionId);
|
|
65
79
|
throwIfAborted(input.signal);
|
|
66
80
|
if (!origin || origin.selfAID !== input.selfAid)
|
|
67
81
|
throw new Error('origin session not found');
|
|
68
82
|
const targetChannelKey = formatChannelKey({ type: 'aun', selfAID: input.selfAid, name: 'main' });
|
|
69
|
-
|
|
83
|
+
if (!input.targetChatType) {
|
|
84
|
+
throw Object.assign(new Error('AUN handoff requires an explicit target chat type'), {
|
|
85
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const targetChatType = input.targetChatType;
|
|
70
89
|
const target = await this.sessionManager.getOrCreateSession(targetChannelKey, input.to, origin.projectPath, input.thread, {
|
|
71
90
|
channelKey: targetChannelKey,
|
|
72
91
|
peerId: input.to,
|
|
@@ -81,24 +100,32 @@ export class HandoffRuntime {
|
|
|
81
100
|
if (input.explicitReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
|
|
82
101
|
throw new TriggerHandoffRequiredForbiddenError();
|
|
83
102
|
}
|
|
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
103
|
const handoffCausation = deriveCausation(normalizeCausation(input.causation) ?? createRootCausation());
|
|
91
|
-
const handoff = this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
const handoff = await this.mutexes.forKey(this.targetKey(input.selfAid, target.id)).runExclusive(async () => {
|
|
105
|
+
const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
|
|
106
|
+
if (replacedSessionId)
|
|
107
|
+
throw new HandoffSessionReplacedError(replacedSessionId);
|
|
108
|
+
this.dispatcher.expireQueued(input.selfAid, target.id);
|
|
109
|
+
throwIfAborted(input.signal);
|
|
110
|
+
const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
|
|
111
|
+
if (head?.attention_required) {
|
|
112
|
+
throw new HandoffTargetBlockedError(head.handoff_id, head.attention_reason ?? 'UNKNOWN_ATTENTION');
|
|
113
|
+
}
|
|
114
|
+
return this.store.create({
|
|
115
|
+
selfAid: input.selfAid,
|
|
116
|
+
originSessionId: origin.id,
|
|
117
|
+
originMessageId: input.originMessageId,
|
|
118
|
+
targetSessionId: target.id,
|
|
119
|
+
payload: input.payload,
|
|
120
|
+
encrypt: input.encrypt,
|
|
121
|
+
delivery: targetChatType === 'group'
|
|
122
|
+
? { chatType: 'group', groupId: input.to }
|
|
123
|
+
: { chatType: 'private' },
|
|
124
|
+
returnPolicy: input.explicitReturnPolicy ?? 'required',
|
|
125
|
+
originAuthorization: input.originAuthorization,
|
|
126
|
+
causation: handoffCausation,
|
|
127
|
+
now: this.now?.(),
|
|
128
|
+
});
|
|
102
129
|
});
|
|
103
130
|
recordCausationSpan(handoffCausation, 'handoff.request', {
|
|
104
131
|
status: 'started',
|
|
@@ -106,6 +133,10 @@ export class HandoffRuntime {
|
|
|
106
133
|
});
|
|
107
134
|
await this.dispatcher.drain(input.selfAid, target.id);
|
|
108
135
|
const dispatched = this.store.get(input.selfAid, handoff.handoff_id) ?? handoff;
|
|
136
|
+
const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
|
|
137
|
+
if (dispatched.state === 'discarded' || replacedSessionId) {
|
|
138
|
+
throw new HandoffSessionReplacedError(replacedSessionId ?? target.id);
|
|
139
|
+
}
|
|
109
140
|
if (dispatched.state === 'failed') {
|
|
110
141
|
throw new HandoffDeliveryFailedError(dispatched.handoff_id);
|
|
111
142
|
}
|
|
@@ -142,7 +173,9 @@ export class HandoffRuntime {
|
|
|
142
173
|
}
|
|
143
174
|
markBindingIncomplete(selfAid, handoffId) {
|
|
144
175
|
const instance = this.store.get(selfAid, handoffId);
|
|
145
|
-
if (instance
|
|
176
|
+
if (instance
|
|
177
|
+
&& (instance.state === 'target_context_pending' || instance.state === 'return_pending')
|
|
178
|
+
&& !instance.attention_required) {
|
|
146
179
|
this.store.markAttention(selfAid, handoffId, 'REPLY_BINDING_INCOMPLETE');
|
|
147
180
|
}
|
|
148
181
|
}
|
|
@@ -185,6 +218,8 @@ export class HandoffRuntime {
|
|
|
185
218
|
messageId,
|
|
186
219
|
timestamp: Date.now(),
|
|
187
220
|
source: 'handoff',
|
|
221
|
+
groupId: origin.chatType === 'group' ? (origin.metadata?.groupId || origin.channelId) : undefined,
|
|
222
|
+
replyContext: replyContextFromSession(origin),
|
|
188
223
|
boundSessionId: origin.id,
|
|
189
224
|
causation: instance.return_causation
|
|
190
225
|
? deriveCausation(instance.return_causation)
|
|
@@ -207,16 +242,31 @@ export class HandoffRuntime {
|
|
|
207
242
|
role: origin.chatType === 'group' ? (origin.identity?.role ?? 'none') : undefined,
|
|
208
243
|
sessionKeyField: origin.sessionKey,
|
|
209
244
|
selfAID: origin.selfAID,
|
|
210
|
-
onPersisted: () =>
|
|
245
|
+
onPersisted: () => {
|
|
246
|
+
if (this.store.get(selfAid, handoffId)?.state !== 'origin_queued') {
|
|
247
|
+
throw new Error(`handoff is no longer deliverable: ${handoffId}`);
|
|
248
|
+
}
|
|
249
|
+
this.store.recordOriginEnqueued(selfAid, handoffId, messageId);
|
|
250
|
+
},
|
|
211
251
|
});
|
|
212
252
|
}
|
|
213
253
|
catch (error) {
|
|
214
|
-
this.store.
|
|
254
|
+
if (this.store.get(selfAid, handoffId)?.state === 'origin_queued') {
|
|
255
|
+
this.store.recordOriginEnqueueFailed(selfAid, handoffId, error instanceof Error ? error.message : String(error));
|
|
256
|
+
}
|
|
215
257
|
}
|
|
216
258
|
}
|
|
217
259
|
buildPromptItem(message) {
|
|
218
260
|
return this.buildPromptItems(message)[0] ?? null;
|
|
219
261
|
}
|
|
262
|
+
isDiscardedDelivery(message) {
|
|
263
|
+
const delivery = message.handoffDelivery;
|
|
264
|
+
const selfAid = message.selfAID;
|
|
265
|
+
if (!delivery || !selfAid)
|
|
266
|
+
return false;
|
|
267
|
+
const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
|
|
268
|
+
return ids.length > 0 && ids.every(handoffId => this.store.get(selfAid, handoffId)?.state === 'discarded');
|
|
269
|
+
}
|
|
220
270
|
buildPromptItems(message) {
|
|
221
271
|
const delivery = message.handoffDelivery;
|
|
222
272
|
const selfAid = message.selfAID;
|
|
@@ -367,9 +417,54 @@ export class HandoffRuntime {
|
|
|
367
417
|
expired += this.dispatcher.expireQueued(selfAid).length;
|
|
368
418
|
return expired;
|
|
369
419
|
}
|
|
370
|
-
async
|
|
420
|
+
async discardSession(selfAid, sessionId) {
|
|
421
|
+
this.sessionDiscardSequence.set(this.sessionKey(selfAid, sessionId), ++this.lifecycleSequence);
|
|
422
|
+
const candidates = this.store.list(selfAid)
|
|
423
|
+
.filter(instance => (instance.origin_session_id === sessionId || instance.target_session_id === sessionId) && instance.state !== 'failed' && instance.state !== 'completed' && instance.state !== 'discarded');
|
|
424
|
+
let discarded = 0;
|
|
425
|
+
for (const instance of candidates) {
|
|
426
|
+
const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'session_replaced', {
|
|
427
|
+
session_id: sessionId,
|
|
428
|
+
});
|
|
429
|
+
if (result)
|
|
430
|
+
discarded++;
|
|
431
|
+
}
|
|
432
|
+
return discarded;
|
|
433
|
+
}
|
|
434
|
+
async discardExpired(selfAids, retentionMs = HANDOFF_RETENTION_MS) {
|
|
435
|
+
const now = this.now?.() ?? Date.now();
|
|
436
|
+
let discarded = 0;
|
|
437
|
+
for (const selfAid of selfAids) {
|
|
438
|
+
const latestEventVersions = new Map();
|
|
439
|
+
for (const event of this.store.events(selfAid)) {
|
|
440
|
+
if (!event.handoff_id || event.to_version === undefined)
|
|
441
|
+
continue;
|
|
442
|
+
latestEventVersions.set(event.handoff_id, Math.max(latestEventVersions.get(event.handoff_id) ?? 0, event.to_version));
|
|
443
|
+
}
|
|
444
|
+
for (const instance of this.store.list(selfAid)) {
|
|
445
|
+
if (instance.state === 'failed' || instance.state === 'completed' || instance.state === 'discarded')
|
|
446
|
+
continue;
|
|
447
|
+
if (now - instance.created_at <= retentionMs)
|
|
448
|
+
continue;
|
|
449
|
+
if ((latestEventVersions.get(instance.handoff_id) ?? 0) !== instance.version)
|
|
450
|
+
continue;
|
|
451
|
+
const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'retention_expired', {
|
|
452
|
+
retention_ms: retentionMs,
|
|
453
|
+
age_ms: now - instance.created_at,
|
|
454
|
+
}, instance.version, now);
|
|
455
|
+
if (result) {
|
|
456
|
+
discarded++;
|
|
457
|
+
logger.info(`[Handoff] discarded expired handoff: ${instance.handoff_id}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return discarded;
|
|
462
|
+
}
|
|
463
|
+
async recover(selfAids, options = {}) {
|
|
371
464
|
const originEnqueues = [];
|
|
372
465
|
for (const selfAid of selfAids) {
|
|
466
|
+
if (options.discardExpired !== false)
|
|
467
|
+
await this.discardExpired([selfAid]);
|
|
373
468
|
// v1 initially represented return=none as return_pending and required an
|
|
374
469
|
// empty manual return. It has already been shown to a target turn, so
|
|
375
470
|
// finish it before version reconciliation instead of prompting again.
|
|
@@ -412,6 +507,40 @@ export class HandoffRuntime {
|
|
|
412
507
|
}
|
|
413
508
|
await Promise.all(originEnqueues);
|
|
414
509
|
}
|
|
510
|
+
async discardWithTargetLock(selfAid, handoffId, reason, details, expectedVersion, now = this.now?.()) {
|
|
511
|
+
const snapshot = this.store.get(selfAid, handoffId);
|
|
512
|
+
if (!snapshot)
|
|
513
|
+
return false;
|
|
514
|
+
const discarded = await this.mutexes.forKey(this.targetKey(selfAid, snapshot.target_session_id)).runExclusive(async () => {
|
|
515
|
+
const current = this.store.get(selfAid, handoffId);
|
|
516
|
+
if (!current || current.state === 'failed' || current.state === 'completed' || current.state === 'discarded') {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
if (expectedVersion !== undefined && current.version !== expectedVersion)
|
|
520
|
+
return null;
|
|
521
|
+
this.store.discard(selfAid, handoffId, reason, details, now);
|
|
522
|
+
return current;
|
|
523
|
+
});
|
|
524
|
+
if (!discarded)
|
|
525
|
+
return false;
|
|
526
|
+
await this.cancelOriginDelivery(discarded);
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
async cancelOriginDelivery(instance) {
|
|
530
|
+
if (instance.state !== 'origin_queued' && instance.state !== 'origin_delivered')
|
|
531
|
+
return;
|
|
532
|
+
const messageId = instance.origin_delivery_message_id
|
|
533
|
+
?? `origin-deliver:${instance.handoff_id}:${instance.origin_session_id}`;
|
|
534
|
+
const queue = this.messageQueue;
|
|
535
|
+
try {
|
|
536
|
+
const queued = queue.cancelMessageByIdInSession?.(instance.origin_session_id, messageId) ?? false;
|
|
537
|
+
if (!queued)
|
|
538
|
+
await queue.cancelActiveAndWait?.(messageId);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
logger.warn(`[Handoff] failed to cancel discarded origin delivery ${messageId}:`, error);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
415
544
|
warnIfStale(instance) {
|
|
416
545
|
if (instance.state !== 'target_sent' && instance.state !== 'return_pending')
|
|
417
546
|
return;
|
|
@@ -425,6 +554,12 @@ export class HandoffRuntime {
|
|
|
425
554
|
+ ` ageHours=${Math.floor(ageMs / 3_600_000)}`
|
|
426
555
|
+ ` originSession=${instance.origin_session_id} targetSession=${instance.target_session_id}`);
|
|
427
556
|
}
|
|
557
|
+
targetKey(selfAid, targetSessionId) {
|
|
558
|
+
return `${selfAid}\u0000${targetSessionId}`;
|
|
559
|
+
}
|
|
560
|
+
sessionKey(selfAid, sessionId) {
|
|
561
|
+
return `${selfAid}\u0000${sessionId}`;
|
|
562
|
+
}
|
|
428
563
|
warnIfLargeAggregate(message, instances, direction) {
|
|
429
564
|
const chars = message.content.length
|
|
430
565
|
+ 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}`);
|
|
@@ -114,7 +115,11 @@ export class HandoffStore {
|
|
|
114
115
|
origin_session_id: input.originSessionId,
|
|
115
116
|
origin_message_id: input.originMessageId,
|
|
116
117
|
target_session_id: input.targetSessionId,
|
|
117
|
-
request: {
|
|
118
|
+
request: {
|
|
119
|
+
payload: clonePayload(input.payload),
|
|
120
|
+
encrypt: input.encrypt,
|
|
121
|
+
...(input.delivery ? { delivery: input.delivery } : {}),
|
|
122
|
+
},
|
|
118
123
|
...(input.originAuthorization
|
|
119
124
|
? { origin_authorization: { ...input.originAuthorization } }
|
|
120
125
|
: {}),
|
|
@@ -514,6 +519,29 @@ export class HandoffStore {
|
|
|
514
519
|
this.write(selfAid, instance);
|
|
515
520
|
return instance;
|
|
516
521
|
}
|
|
522
|
+
discard(selfAid, handoffId, reason, details = {}, now = Date.now()) {
|
|
523
|
+
const instance = this.get(selfAid, handoffId);
|
|
524
|
+
if (!instance)
|
|
525
|
+
throw new Error('handoff not found');
|
|
526
|
+
if (TERMINAL_HANDOFF_STATES.has(instance.state))
|
|
527
|
+
return instance;
|
|
528
|
+
const previousState = instance.state;
|
|
529
|
+
const previousVersion = instance.version;
|
|
530
|
+
instance.state = 'discarded';
|
|
531
|
+
instance.attention_required = false;
|
|
532
|
+
instance.attention_reason = null;
|
|
533
|
+
instance.version++;
|
|
534
|
+
instance.updated_at = now;
|
|
535
|
+
instance.completed_at = now;
|
|
536
|
+
this.appendEvent(selfAid, 'handoff_discarded', handoffId, {
|
|
537
|
+
fromVersion: previousVersion,
|
|
538
|
+
toVersion: instance.version,
|
|
539
|
+
details: { ...details, reason, previous_state: previousState },
|
|
540
|
+
now,
|
|
541
|
+
});
|
|
542
|
+
this.write(selfAid, instance);
|
|
543
|
+
return instance;
|
|
544
|
+
}
|
|
517
545
|
requireState(selfAid, handoffId, expected) {
|
|
518
546
|
const instance = this.get(selfAid, handoffId);
|
|
519
547
|
if (!instance)
|
|
@@ -228,6 +228,16 @@ export class InteractionRouter {
|
|
|
228
228
|
return undefined;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
+
/** Bind an adapter response to its runtime-owned channel identity domain. */
|
|
232
|
+
export function registerAdapterInteractions(adapter, router) {
|
|
233
|
+
const channelKey = adapter.channelKey;
|
|
234
|
+
adapter.onInteraction?.(async (response) => {
|
|
235
|
+
return router.handleAsync({ ...response, channelKey });
|
|
236
|
+
});
|
|
237
|
+
adapter.onInteractionInvalidated?.((interactionId, reason) => {
|
|
238
|
+
return router.cancelFromChannel(interactionId, reason);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
231
241
|
/** 把 CommandCard 渲染为文本提示,channel 不支持卡片时使用 */
|
|
232
242
|
export function renderCommandCardAsText(card) {
|
|
233
243
|
const lines = [card.title];
|
|
@@ -244,6 +244,25 @@ export class IMRenderer {
|
|
|
244
244
|
durationMs: opts.durationMs,
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
|
+
/** 发送不进入 activity/thought 历史的非终态 warning status。 */
|
|
248
|
+
async sendWarningStatus(message, subtype, details) {
|
|
249
|
+
const payload = {
|
|
250
|
+
kind: 'status.progress',
|
|
251
|
+
metadata: {
|
|
252
|
+
activityType: 'progress',
|
|
253
|
+
message,
|
|
254
|
+
severity: 'warn',
|
|
255
|
+
...(subtype !== undefined && { subtype }),
|
|
256
|
+
...(details?.missingItems !== undefined && { missingItems: details.missingItems }),
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
await this.opts.send(payload);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
logger.warn(`[IMRenderer] warning status send failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
247
266
|
/** 添加任务生命周期边界事件,复用 activity.batch 投影路径。 */
|
|
248
267
|
addLifecycle(phase, metadata, text) {
|
|
249
268
|
const item = {
|
|
@@ -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
|
+
}
|