evolcore 0.0.21 → 0.0.22
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 +43 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/claude-runner.js +14 -0
- package/dist/agents/codex-app-server-client.js +31 -5
- package/dist/agents/codex-runner.js +926 -121
- package/dist/aun/outbox.js +7 -0
- package/dist/channels/aun.js +209 -35
- package/dist/cli/daemon-commands.js +29 -8
- package/dist/cli/task-context.js +4 -0
- package/dist/cli/trigger-command.js +13 -4
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/core/auth/agent-delegation.js +12 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +66 -6
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +21 -0
- package/dist/core/command/menu-handler.js +9 -0
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/slash-handler.js +41 -18
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +21 -0
- package/dist/core/message/response-engine.js +172 -41
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +11 -10
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +46 -5
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +64 -5
- package/dist/ipc.js +97 -17
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/parser.js +10 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/logger.js +9 -4
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +2 -2
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +2 -2
|
@@ -14,6 +14,7 @@ const LOCK_NAME = '.contact-book-mutation.lock';
|
|
|
14
14
|
const JOURNAL_NAME = '.contact-book-mutation.json';
|
|
15
15
|
const LOCK_WAIT_MS = 5_000;
|
|
16
16
|
const LOCK_RETRY_MS = 20;
|
|
17
|
+
const CORRUPT_LOCK_STALE_MS = LOCK_WAIT_MS;
|
|
17
18
|
export const EMPTY_CONTACT_REVISION = crypto.createHash('sha256').update('').digest('hex');
|
|
18
19
|
export class ContactMutationError extends Error {
|
|
19
20
|
code;
|
|
@@ -454,7 +455,10 @@ function acquireAgentLock(selfAid) {
|
|
|
454
455
|
return () => releaseAgentLock(lockDir, ownerFile, lockToken);
|
|
455
456
|
}
|
|
456
457
|
catch (error) {
|
|
457
|
-
|
|
458
|
+
const lockMayAlreadyExist = error?.code === 'EEXIST'
|
|
459
|
+
|| error?.code === 'ENOTEMPTY'
|
|
460
|
+
|| (process.platform === 'win32' && error?.code === 'EPERM' && fs.existsSync(lockDir));
|
|
461
|
+
if (!lockMayAlreadyExist)
|
|
458
462
|
throw error;
|
|
459
463
|
if (removeStaleLock(lockDir, ownerFile))
|
|
460
464
|
continue;
|
|
@@ -478,10 +482,10 @@ function removeStaleLock(lockDir, ownerFile) {
|
|
|
478
482
|
pid = Number(JSON.parse(rawOwner).pid);
|
|
479
483
|
}
|
|
480
484
|
catch {
|
|
481
|
-
return
|
|
485
|
+
return reclaimCorruptLock(lockDir);
|
|
482
486
|
}
|
|
483
487
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
484
|
-
return
|
|
488
|
+
return reclaimCorruptLock(lockDir);
|
|
485
489
|
try {
|
|
486
490
|
process.kill(pid, 0);
|
|
487
491
|
return false;
|
|
@@ -503,6 +507,24 @@ function removeStaleLock(lockDir, ownerFile) {
|
|
|
503
507
|
}
|
|
504
508
|
}
|
|
505
509
|
}
|
|
510
|
+
function reclaimCorruptLock(lockDir) {
|
|
511
|
+
try {
|
|
512
|
+
const stat = fs.statSync(lockDir);
|
|
513
|
+
if (Date.now() - stat.mtimeMs < CORRUPT_LOCK_STALE_MS)
|
|
514
|
+
return false;
|
|
515
|
+
const staleClaim = `${lockDir}.reclaimed-corrupt-${crypto.randomBytes(8).toString('hex')}`;
|
|
516
|
+
fs.renameSync(lockDir, staleClaim);
|
|
517
|
+
fs.rmSync(staleClaim, { recursive: true, force: true });
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
if (error?.code === 'ENOENT')
|
|
522
|
+
return true;
|
|
523
|
+
if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY' || error?.code === 'EPERM')
|
|
524
|
+
return false;
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
506
528
|
function releaseAgentLock(lockDir, ownerFile, lockToken) {
|
|
507
529
|
try {
|
|
508
530
|
const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
|
|
@@ -4,9 +4,13 @@ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
|
4
4
|
export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
|
|
5
5
|
export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
|
|
6
6
|
export class AgentDelegationRegistry {
|
|
7
|
+
runtimeEpoch = crypto.randomBytes(16).toString('hex');
|
|
7
8
|
grantsByHash = new Map();
|
|
8
9
|
activeHashBySession = new Map();
|
|
9
10
|
approvedCommands = new Map();
|
|
11
|
+
getRuntimeEpoch() {
|
|
12
|
+
return this.runtimeEpoch;
|
|
13
|
+
}
|
|
10
14
|
issue(input) {
|
|
11
15
|
if (input.executionIdentity && !isValidFullAccessExecutionIdentity(input.executionIdentity)) {
|
|
12
16
|
throw new Error('invalid fullaccess task execution identity');
|
|
@@ -24,6 +28,7 @@ export class AgentDelegationRegistry {
|
|
|
24
28
|
ok: false,
|
|
25
29
|
code: 'DELEGATION_REQUIRED',
|
|
26
30
|
reason: 'An active task delegation token is required',
|
|
31
|
+
diagnosticCause: 'missing_token',
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
const tokenHash = hashDelegationToken(token);
|
|
@@ -42,6 +47,13 @@ export class AgentDelegationRegistry {
|
|
|
42
47
|
ok: false,
|
|
43
48
|
code: 'INVALID_DELEGATION',
|
|
44
49
|
reason: 'The task delegation token is invalid, revoked, or belongs to another session',
|
|
50
|
+
diagnosticCause: grant
|
|
51
|
+
? grant.sessionId !== sessionId
|
|
52
|
+
? 'session_mismatch'
|
|
53
|
+
: 'task_replaced'
|
|
54
|
+
: commandHash
|
|
55
|
+
? 'approved_command_unavailable'
|
|
56
|
+
: 'unknown_token',
|
|
45
57
|
};
|
|
46
58
|
}
|
|
47
59
|
armApprovedCommand(input) {
|
|
@@ -192,6 +192,14 @@ function auditDecision(params, decision) {
|
|
|
192
192
|
permissionMode: params.auditMetadata?.permissionMode
|
|
193
193
|
?? (decision.allow ? decision.permissionMode ?? params.subject.permissionMode : params.subject.permissionMode),
|
|
194
194
|
toolName: params.auditMetadata?.toolName,
|
|
195
|
+
errorCode: params.auditMetadata?.errorCode
|
|
196
|
+
?? (decision.allow
|
|
197
|
+
? undefined
|
|
198
|
+
: params.auditMetadata?.decisionSource === 'approval'
|
|
199
|
+
? 'USER_DENIED'
|
|
200
|
+
: params.auditMetadata?.decisionSource === 'infrastructure'
|
|
201
|
+
? 'EXECUTION_FAILED'
|
|
202
|
+
: 'POLICY_DENIED'),
|
|
195
203
|
policyCode: params.auditMetadata?.policyCode ?? (decision.allow ? undefined : decision.code),
|
|
196
204
|
decisionSource: params.auditMetadata?.decisionSource ?? (decision.allow ? 'command' : 'policy'),
|
|
197
205
|
source: params.source,
|
|
@@ -2,7 +2,7 @@ import { logger } from '../../utils/logger.js';
|
|
|
2
2
|
import { LogWriter } from '../../utils/log-writer.js';
|
|
3
3
|
import { resolvePaths } from '../../paths.js';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
|
-
import { buildAuthorizationEventKey } from '../audit/event-key.js';
|
|
5
|
+
import { buildAuthorizationEventKey, buildToolLifecycleEventKey } from '../audit/event-key.js';
|
|
6
6
|
import { normalizeExecutionPermissionMode } from '../permission/mode.js';
|
|
7
7
|
export function auditCommandAuthorization(event) {
|
|
8
8
|
const shouldAudit = event.source === 'menu.cli' ||
|
|
@@ -95,10 +95,20 @@ export function auditFullAccessEvent(input) {
|
|
|
95
95
|
* denials otherwise only reach `logger.warn` and `TriggerExecutionAnomaly`.
|
|
96
96
|
*/
|
|
97
97
|
export function auditToolPreflightDenial(input) {
|
|
98
|
+
const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
|
|
98
99
|
auditCommandAuthorization({
|
|
99
100
|
ts: Date.now(),
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
...(lifecycleId ? {
|
|
102
|
+
eventKey: buildToolLifecycleEventKey({
|
|
103
|
+
sessionId: input.sessionId,
|
|
104
|
+
callId: input.callId,
|
|
105
|
+
correlationId: input.correlationId,
|
|
106
|
+
requestId: input.requestId,
|
|
107
|
+
}),
|
|
108
|
+
eventPhase: 'preflight',
|
|
109
|
+
} : {}),
|
|
110
|
+
callId: input.callId ?? input.requestId,
|
|
111
|
+
correlationId: input.correlationId ?? input.callId ?? input.requestId,
|
|
102
112
|
source: 'agent-tool',
|
|
103
113
|
operation: 'tool.preflight.deny',
|
|
104
114
|
scope: 'filesystem',
|
|
@@ -107,6 +117,7 @@ export function auditToolPreflightDenial(input) {
|
|
|
107
117
|
executed: false,
|
|
108
118
|
executionState: 'blocked',
|
|
109
119
|
decisionSource: 'policy',
|
|
120
|
+
errorCode: 'POLICY_DENIED',
|
|
110
121
|
toolName: input.toolName,
|
|
111
122
|
policyCode: input.policyCode,
|
|
112
123
|
protectionClass: input.protectionClass ?? protectionClassForPolicy(input.policyCode),
|
|
@@ -135,9 +146,19 @@ export function auditToolPreflightDenial(input) {
|
|
|
135
146
|
* decision source make policy, user, and infrastructure denials distinct.
|
|
136
147
|
*/
|
|
137
148
|
export function auditCodexApprovalDecision(input) {
|
|
149
|
+
const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
|
|
138
150
|
auditCommandAuthorization({
|
|
139
151
|
ts: Date.now(),
|
|
140
|
-
|
|
152
|
+
...(lifecycleId ? {
|
|
153
|
+
eventKey: buildToolLifecycleEventKey({
|
|
154
|
+
sessionId: input.sessionId,
|
|
155
|
+
callId: input.callId,
|
|
156
|
+
correlationId: input.correlationId,
|
|
157
|
+
requestId: input.requestId,
|
|
158
|
+
}),
|
|
159
|
+
eventPhase: 'approval',
|
|
160
|
+
} : {}),
|
|
161
|
+
callId: input.callId ?? input.correlationId ?? input.requestId,
|
|
141
162
|
source: 'agent-tool',
|
|
142
163
|
operation: 'codex.approval',
|
|
143
164
|
scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
|
|
@@ -146,8 +167,11 @@ export function auditCodexApprovalDecision(input) {
|
|
|
146
167
|
executed: false,
|
|
147
168
|
executionState: input.decision === 'allow' ? 'authorized' : 'blocked',
|
|
148
169
|
decisionSource: input.decisionSource,
|
|
170
|
+
errorCode: input.decision === 'deny'
|
|
171
|
+
? input.decisionSource === 'approval' ? 'USER_DENIED' : input.decisionSource === 'policy' ? 'POLICY_DENIED' : infrastructureErrorCode(input.policyCode)
|
|
172
|
+
: undefined,
|
|
149
173
|
requestId: input.requestId,
|
|
150
|
-
correlationId: input.correlationId ?? input.requestId,
|
|
174
|
+
correlationId: input.correlationId ?? input.callId ?? input.requestId,
|
|
151
175
|
sessionId: input.sessionId ?? 'unknown',
|
|
152
176
|
agentAid: input.agentAid ?? 'unknown',
|
|
153
177
|
agentName: input.agentName,
|
|
@@ -183,6 +207,11 @@ export function auditPermissionDecision(input) {
|
|
|
183
207
|
toolName: input.toolName,
|
|
184
208
|
policyCode: input.policyCode,
|
|
185
209
|
decisionSource: input.decisionSource,
|
|
210
|
+
errorCode: input.decisionSource === 'approval'
|
|
211
|
+
? 'USER_DENIED'
|
|
212
|
+
: input.decisionSource === 'policy'
|
|
213
|
+
? 'POLICY_DENIED'
|
|
214
|
+
: infrastructureErrorCode(input.policyCode),
|
|
186
215
|
source: 'agent-tool',
|
|
187
216
|
operation: 'permission.runtime',
|
|
188
217
|
scope: 'filesystem',
|
|
@@ -205,6 +234,7 @@ export function auditToolInfrastructureFailure(input) {
|
|
|
205
234
|
dangerous: false,
|
|
206
235
|
decision: 'deny',
|
|
207
236
|
decisionSource: 'infrastructure',
|
|
237
|
+
errorCode: infrastructureErrorCode(input.policyCode),
|
|
208
238
|
executed: false,
|
|
209
239
|
executionState: 'blocked',
|
|
210
240
|
callId: input.callId,
|
|
@@ -224,8 +254,26 @@ export function auditToolInfrastructureFailure(input) {
|
|
|
224
254
|
: undefined,
|
|
225
255
|
});
|
|
226
256
|
}
|
|
257
|
+
function infrastructureErrorCode(policyCode) {
|
|
258
|
+
const code = String(policyCode ?? '').toLowerCase();
|
|
259
|
+
if (/delegat|carrier|not.?armed/.test(code))
|
|
260
|
+
return 'DELEGATION_FAILED';
|
|
261
|
+
if (/capability|sandbox|unavailable|workspace_unknown/.test(code))
|
|
262
|
+
return 'CAPABILITY_UNAVAILABLE';
|
|
263
|
+
if (/argument|canonical|quote|shell|parse|invalid/.test(code))
|
|
264
|
+
return 'INVALID_ARGUMENT';
|
|
265
|
+
return 'EXECUTION_FAILED';
|
|
266
|
+
}
|
|
227
267
|
function buildAuditRecord(event) {
|
|
228
268
|
const executed = event.executed ?? false;
|
|
269
|
+
const decisionSource = event.decisionSource ?? (event.decision === 'deny' ? 'policy' : 'command');
|
|
270
|
+
const errorCode = event.errorCode ?? (event.decision === 'deny'
|
|
271
|
+
? decisionSource === 'approval'
|
|
272
|
+
? 'USER_DENIED'
|
|
273
|
+
: decisionSource === 'policy'
|
|
274
|
+
? 'POLICY_DENIED'
|
|
275
|
+
: 'EXECUTION_FAILED'
|
|
276
|
+
: undefined);
|
|
229
277
|
const executionState = event.executionState
|
|
230
278
|
?? (event.decision === 'deny'
|
|
231
279
|
? 'blocked'
|
|
@@ -244,6 +292,7 @@ function buildAuditRecord(event) {
|
|
|
244
292
|
operation: event.operation,
|
|
245
293
|
})
|
|
246
294
|
: undefined),
|
|
295
|
+
eventPhase: event.eventPhase,
|
|
247
296
|
callId: event.callId,
|
|
248
297
|
correlationId: event.correlationId ?? event.requestId,
|
|
249
298
|
requestId: event.requestId,
|
|
@@ -253,10 +302,15 @@ function buildAuditRecord(event) {
|
|
|
253
302
|
permissionMode: normalizeAuditPermissionMode(event.permissionMode),
|
|
254
303
|
toolName: event.toolName,
|
|
255
304
|
approvalMethod: event.approvalMethod,
|
|
305
|
+
errorCode,
|
|
256
306
|
policyCode: event.policyCode ?? event.code,
|
|
257
307
|
protectionClass: event.protectionClass,
|
|
258
308
|
matchedPath: event.matchedPath,
|
|
259
|
-
decisionSource
|
|
309
|
+
decisionSource,
|
|
310
|
+
ecwebRequestId: event.ecwebRequestId,
|
|
311
|
+
targetAgentAid: redactIdentifier(event.targetAgentAid),
|
|
312
|
+
endpoint: event.endpoint,
|
|
313
|
+
logicalToolName: event.logicalToolName,
|
|
260
314
|
source: event.source,
|
|
261
315
|
operation: event.operation,
|
|
262
316
|
scope: event.scope,
|
|
@@ -343,11 +397,17 @@ function logAuditEvent(record) {
|
|
|
343
397
|
record.callId ? `call=${record.callId}` : null,
|
|
344
398
|
record.correlationId ? `correlation=${record.correlationId}` : null,
|
|
345
399
|
record.eventKey ? `eventKey=${record.eventKey}` : null,
|
|
400
|
+
record.eventPhase ? `eventPhase=${record.eventPhase}` : null,
|
|
346
401
|
record.sessionId ? `session=${record.sessionId}` : null,
|
|
347
402
|
record.agentName ? `agentName=${JSON.stringify(record.agentName)}` : null,
|
|
348
403
|
record.agentAid ? `agent=${record.agentAid}` : null,
|
|
349
404
|
record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
|
|
350
405
|
record.toolName ? `tool=${record.toolName}` : null,
|
|
406
|
+
record.errorCode ? `errorCode=${record.errorCode}` : null,
|
|
407
|
+
record.ecwebRequestId ? `ecwebRequest=${record.ecwebRequestId}` : null,
|
|
408
|
+
record.targetAgentAid ? `targetAgent=${record.targetAgentAid}` : null,
|
|
409
|
+
record.endpoint ? `endpoint=${JSON.stringify(record.endpoint)}` : null,
|
|
410
|
+
record.logicalToolName ? `logicalTool=${record.logicalToolName}` : null,
|
|
351
411
|
record.policyCode ? `policy=${record.policyCode}` : null,
|
|
352
412
|
record.protectionClass ? `protectionClass=${record.protectionClass}` : null,
|
|
353
413
|
record.matchedPath ? `matchedPath=${JSON.stringify(record.matchedPath)}` : null,
|
|
@@ -7,9 +7,17 @@ import { chatDirPath } from './session/session-fs-store.js';
|
|
|
7
7
|
import { hasMessageLogOperation } from './message/message-log.js';
|
|
8
8
|
import { isDeliveryTarget, sameDeliveryTarget } from './message/message-utils.js';
|
|
9
9
|
export const BOOTSTRAP_MESSAGE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
10
|
+
export const BOOTSTRAP_NOTIFICATION_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
10
11
|
export function bootstrapInitialMessageOperationId(aid) {
|
|
11
12
|
return `bootstrap-initial:v1:${aid}`;
|
|
12
13
|
}
|
|
14
|
+
export function bootstrapInitialMessageOperationPrefix(aid) {
|
|
15
|
+
return `bootstrap-initial:v1:${aid}`;
|
|
16
|
+
}
|
|
17
|
+
/** A new operation ID is required after the notification window expires. */
|
|
18
|
+
export function bootstrapInitialRetryOperationId(aid, at = Date.now()) {
|
|
19
|
+
return `${bootstrapInitialMessageOperationPrefix(aid)}:${at}`;
|
|
20
|
+
}
|
|
13
21
|
export function postBootstrapWelcomeOperationId(aid) {
|
|
14
22
|
return `bootstrap-complete:v1:${aid}`;
|
|
15
23
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
|
-
import { kitsTemplatesDir, agentMdPath } from '../paths.js';
|
|
4
|
+
import { kitsTemplatesDir, agentMdPath, resolvePaths } from '../paths.js';
|
|
5
5
|
import { buildInitialAgentMd, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
import { loadAgent, saveAgent } from '../config-store.js';
|
|
@@ -9,7 +9,10 @@ import { resolveAgentLifecycle, withLifecycleForWrite } from '../config/lifecycl
|
|
|
9
9
|
import { renderTemplate } from '../eck/manifest-engine.js';
|
|
10
10
|
import { activeBaseagent } from './model/config-scope.js';
|
|
11
11
|
import { buildEnvelope } from './message/message-utils.js';
|
|
12
|
-
import { BOOTSTRAP_MESSAGE_TTL_MS, bootstrapInitialMessageOperationId } from './bootstrap-messages.js';
|
|
12
|
+
import { BOOTSTRAP_MESSAGE_TTL_MS, BOOTSTRAP_NOTIFICATION_WINDOW_MS, bootstrapInitialMessageOperationId, bootstrapInitialMessageOperationPrefix, bootstrapInitialRetryOperationId, } from './bootstrap-messages.js';
|
|
13
|
+
import { findLatestMessageLogOperation } from './message/message-log.js';
|
|
14
|
+
import * as outbox from '../aun/outbox.js';
|
|
15
|
+
import { chatDirPath } from './session/session-fs-store.js';
|
|
13
16
|
/** Coordinate the write-ahead completion welcome with the lifecycle commit. */
|
|
14
17
|
export async function completeBootstrapWithWelcome(service, aid, welcome, sessions) {
|
|
15
18
|
const lifecycle = service.lifecycleOf(aid);
|
|
@@ -102,6 +105,7 @@ export class BootstrapService {
|
|
|
102
105
|
agentRegistry;
|
|
103
106
|
eventBus;
|
|
104
107
|
inFlight = new Set();
|
|
108
|
+
notifiedInProcess = new Map();
|
|
105
109
|
constructor(agentRegistry, eventBus) {
|
|
106
110
|
this.agentRegistry = agentRegistry;
|
|
107
111
|
this.eventBus = eventBus;
|
|
@@ -161,41 +165,58 @@ export class BootstrapService {
|
|
|
161
165
|
try {
|
|
162
166
|
const agentName = this.resolveAgentDisplayName(aid);
|
|
163
167
|
const baseagent = this.resolveBaseagent(agent, aid);
|
|
168
|
+
const notificationState = this.bootstrapNotificationState({
|
|
169
|
+
aid,
|
|
170
|
+
channelType,
|
|
171
|
+
channelId,
|
|
172
|
+
channelKey: ctx.channelKey,
|
|
173
|
+
});
|
|
174
|
+
const operationId = starting && !notificationState.hasCompletedHistory
|
|
175
|
+
? bootstrapInitialMessageOperationId(aid)
|
|
176
|
+
: bootstrapInitialRetryOperationId(aid);
|
|
177
|
+
const shouldSend = !notificationState.recentlyNotified;
|
|
164
178
|
if (starting) {
|
|
165
179
|
await this.publishAgentMdIfSupported(ctx.adapter, aid, agentName);
|
|
166
180
|
this.setLifecycle(agent, aid, 'bootstrapping');
|
|
167
181
|
lifecycleStarted = true;
|
|
168
182
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
if (shouldSend) {
|
|
184
|
+
const text = this.renderWelcome({
|
|
185
|
+
agentAid: aid,
|
|
186
|
+
agentName,
|
|
187
|
+
ownerName: ctx.recipientName || recipientId,
|
|
188
|
+
channel: channelType || ctx.channelKey,
|
|
189
|
+
baseagent,
|
|
190
|
+
});
|
|
191
|
+
await ctx.adapter.send(buildEnvelope({
|
|
192
|
+
taskId: `bootstrap-${randomBytes(5).toString('hex')}`,
|
|
193
|
+
operationId,
|
|
194
|
+
channel: ctx.adapter.channelKey || ctx.adapter.channelName,
|
|
195
|
+
channelId,
|
|
196
|
+
agentName: aid,
|
|
197
|
+
replyContext: {
|
|
198
|
+
delivery,
|
|
199
|
+
metadata: {
|
|
200
|
+
source: 'daemon',
|
|
201
|
+
persistRequired: true,
|
|
202
|
+
operationId,
|
|
203
|
+
outboxTtl: BOOTSTRAP_MESSAGE_TTL_MS,
|
|
204
|
+
criticalDelivery: true,
|
|
205
|
+
},
|
|
189
206
|
},
|
|
190
|
-
},
|
|
191
|
-
|
|
207
|
+
}), { kind: 'result.text', text, isFinal: true });
|
|
208
|
+
this.notifiedInProcess.set(aid, Date.now());
|
|
209
|
+
}
|
|
192
210
|
if (starting) {
|
|
193
211
|
this.eventBus.publish({ type: 'agent:bootstrap-started', aid, channel: channelType || ctx.channelKey, timestamp: Date.now() });
|
|
194
212
|
logger.info(`[Bootstrap] Started for ${aid} via ${ctx.channelKey} (${ctx.source})`);
|
|
195
213
|
}
|
|
196
|
-
else {
|
|
214
|
+
else if (shouldSend) {
|
|
197
215
|
logger.info(`[Bootstrap] Reconciled initial message for ${aid} via ${ctx.channelKey} (${ctx.source})`);
|
|
198
216
|
}
|
|
217
|
+
else {
|
|
218
|
+
logger.debug(`[Bootstrap] Initial message already sent within the notification window for ${aid}`);
|
|
219
|
+
}
|
|
199
220
|
return true;
|
|
200
221
|
}
|
|
201
222
|
catch (e) {
|
|
@@ -288,6 +309,53 @@ export class BootstrapService {
|
|
|
288
309
|
return 'unknown';
|
|
289
310
|
}
|
|
290
311
|
}
|
|
312
|
+
bootstrapNotificationState(input) {
|
|
313
|
+
const prefix = bootstrapInitialMessageOperationPrefix(input.aid);
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
const cutoff = now - BOOTSTRAP_NOTIFICATION_WINDOW_MS;
|
|
316
|
+
let latestTs = this.notifiedInProcess.get(input.aid) ?? 0;
|
|
317
|
+
let latestLoggedOperationId;
|
|
318
|
+
try {
|
|
319
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, input.channelType || 'aun', input.channelId, input.aid, input.channelKey);
|
|
320
|
+
let latestLogged = findLatestMessageLogOperation(chatDir, prefix);
|
|
321
|
+
// Releases before the stable envelope operation ID fix persisted the
|
|
322
|
+
// random bootstrap task ID instead. Recognize that exact legacy shape
|
|
323
|
+
// so an upgrade/restart does not immediately resend the welcome.
|
|
324
|
+
if (!latestLogged) {
|
|
325
|
+
const legacyLogged = findLatestMessageLogOperation(chatDir, 'bootstrap-');
|
|
326
|
+
if (legacyLogged && /^bootstrap-[a-f0-9]{10}$/u.test(legacyLogged.operationId)) {
|
|
327
|
+
latestLogged = legacyLogged;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (latestLogged) {
|
|
331
|
+
latestTs = Math.max(latestTs, latestLogged.ts);
|
|
332
|
+
latestLoggedOperationId = latestLogged.operationId;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// A missing or malformed session path must not prevent bootstrap.
|
|
337
|
+
}
|
|
338
|
+
for (const entry of outbox.findByDedupePrefix(input.aid, prefix, { includeTerminal: true })) {
|
|
339
|
+
// A completed operation no longer needs its queued duplicate. Entries
|
|
340
|
+
// older than the notification window must not be delivered alongside
|
|
341
|
+
// the newly generated reminder operation.
|
|
342
|
+
if (entry.terminal) {
|
|
343
|
+
outbox.remove(input.aid, entry.id);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (entry.dedupeKey === latestLoggedOperationId || entry.ts < cutoff) {
|
|
347
|
+
outbox.remove(input.aid, entry.id);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
latestTs = Math.max(latestTs, entry.ts || 0);
|
|
351
|
+
}
|
|
352
|
+
const recentlyNotified = latestTs >= cutoff;
|
|
353
|
+
if (recentlyNotified)
|
|
354
|
+
this.notifiedInProcess.set(input.aid, latestTs);
|
|
355
|
+
else
|
|
356
|
+
this.notifiedInProcess.delete(input.aid);
|
|
357
|
+
return { recentlyNotified, hasCompletedHistory: !!latestLoggedOperationId };
|
|
358
|
+
}
|
|
291
359
|
async publishAgentMdIfSupported(adapter, aid, fallbackName) {
|
|
292
360
|
if (typeof adapter.uploadAgentMd !== 'function')
|
|
293
361
|
return;
|
|
@@ -1119,6 +1119,27 @@ export class CommandHandler {
|
|
|
1119
1119
|
async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
|
|
1120
1120
|
try {
|
|
1121
1121
|
const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
|
|
1122
|
+
// Lifecycle commands are control-plane operations. Their replies must
|
|
1123
|
+
// use notification payloads so other agents in a group do not treat the
|
|
1124
|
+
// text as ordinary model input. Normalize guard-path replies here too,
|
|
1125
|
+
// since those return before slash-handler command-specific branches.
|
|
1126
|
+
if (/^\/(?:new|renew)(?:\s|$)/i.test(content.trim())
|
|
1127
|
+
&& result && typeof result === 'object'
|
|
1128
|
+
&& (result.kind === 'command.result' || result.kind === 'command.error')) {
|
|
1129
|
+
const isError = result.kind === 'command.error';
|
|
1130
|
+
return isError
|
|
1131
|
+
? {
|
|
1132
|
+
kind: 'system.error',
|
|
1133
|
+
text: result.text,
|
|
1134
|
+
subtype: `session.${content.trim().toLowerCase().startsWith('/new') ? 'new' : 'renew'}.error`,
|
|
1135
|
+
recoverable: true,
|
|
1136
|
+
}
|
|
1137
|
+
: {
|
|
1138
|
+
kind: 'system.notice',
|
|
1139
|
+
text: result.text,
|
|
1140
|
+
subtype: `session.${content.trim().toLowerCase().startsWith('/new') ? 'new' : 'renew'}.result`,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1122
1143
|
return result;
|
|
1123
1144
|
}
|
|
1124
1145
|
catch (error) {
|
|
@@ -6,6 +6,7 @@ import { hasModelSwitcher } from '../../agents/runner-types.js';
|
|
|
6
6
|
import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
7
7
|
import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
|
|
8
8
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
9
|
+
import { inspectDataMigrationRequirement } from '../data-migration.js';
|
|
9
10
|
import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
|
|
10
11
|
import path from 'path';
|
|
11
12
|
import fs from 'fs';
|
|
@@ -3563,6 +3564,14 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3563
3564
|
: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
3564
3565
|
};
|
|
3565
3566
|
}
|
|
3567
|
+
const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
|
|
3568
|
+
if (migrationRequirement.required) {
|
|
3569
|
+
return {
|
|
3570
|
+
error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
|
|
3571
|
+
code: 'DATA_MIGRATION_REQUIRED',
|
|
3572
|
+
data: { reasonCode: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount },
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3566
3575
|
if (!suppressRealRestart) {
|
|
3567
3576
|
const restartInfo = {
|
|
3568
3577
|
channel,
|
|
@@ -272,7 +272,7 @@ const STABLE_CODES = new Set([
|
|
|
272
272
|
'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE', 'UPGRADE_REQUIRED',
|
|
273
273
|
'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
|
|
274
274
|
'INVALID_CONTEXT', 'SCHEMA_VERSION_UNSUPPORTED', 'DEPENDENCY_UNAVAILABLE',
|
|
275
|
-
'RATE_LIMITED', 'CATALOG_STALE',
|
|
275
|
+
'RATE_LIMITED', 'CATALOG_STALE', 'DATA_MIGRATION_REQUIRED',
|
|
276
276
|
]);
|
|
277
277
|
export function normalizeMenuError(error) {
|
|
278
278
|
const source = error && typeof error === 'object' ? error : {};
|