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.
Files changed (64) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/bin/codex-managed-hook.mjs +3 -0
  3. package/bin/install-codex-managed-hooks.mjs +3 -1
  4. package/dist/agents/claude-runner.js +14 -0
  5. package/dist/agents/codex-app-server-client.js +31 -5
  6. package/dist/agents/codex-runner.js +926 -121
  7. package/dist/aun/outbox.js +7 -0
  8. package/dist/channels/aun.js +209 -35
  9. package/dist/cli/daemon-commands.js +29 -8
  10. package/dist/cli/task-context.js +4 -0
  11. package/dist/cli/trigger-command.js +13 -4
  12. package/dist/config/config-field-policy.js +3 -0
  13. package/dist/config/config-manager.js +32 -5
  14. package/dist/config/contact-book-store.js +25 -3
  15. package/dist/core/auth/agent-delegation.js +12 -0
  16. package/dist/core/auth/auth-gateway.js +8 -0
  17. package/dist/core/auth/authorization-audit.js +66 -6
  18. package/dist/core/bootstrap-messages.js +8 -0
  19. package/dist/core/bootstrap-service.js +93 -25
  20. package/dist/core/command/command-handler.js +21 -0
  21. package/dist/core/command/menu-handler.js +9 -0
  22. package/dist/core/command/menu-protocol.js +1 -1
  23. package/dist/core/command/slash-handler.js +41 -18
  24. package/dist/core/data-migration.js +11 -1
  25. package/dist/core/event-catalog.js +32 -0
  26. package/dist/core/handoff/runtime.js +23 -3
  27. package/dist/core/message/im-renderer.js +7 -3
  28. package/dist/core/message/message-bridge.js +60 -2
  29. package/dist/core/message/message-log.js +33 -0
  30. package/dist/core/message/message-queue.js +21 -0
  31. package/dist/core/message/response-engine.js +172 -41
  32. package/dist/core/permission/ec-command-parser.js +272 -70
  33. package/dist/core/permission/protected-paths.js +11 -10
  34. package/dist/core/permission/tool-error-code.js +12 -0
  35. package/dist/core/permission/tool-policy.js +46 -5
  36. package/dist/core/session/session-manager.js +30 -0
  37. package/dist/core/session/session-renew.js +18 -1
  38. package/dist/core/session/session-turn-coordinator.js +5 -1
  39. package/dist/index.js +64 -5
  40. package/dist/ipc.js +97 -17
  41. package/dist/paths.js +18 -0
  42. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  43. package/dist/stats/price-resolver.js +4 -0
  44. package/dist/trigger/feedback.js +14 -2
  45. package/dist/trigger/parser.js +10 -1
  46. package/dist/trigger/scheduler.js +20 -3
  47. package/dist/utils/logger.js +9 -4
  48. package/dist/utils/tool-summary.js +59 -0
  49. package/dist/utils/windows-shell-trust.js +201 -0
  50. package/kits/docs/evolcore/INDEX.md +2 -2
  51. package/kits/docs/evolcore/agent-create.md +146 -0
  52. package/kits/docs/evolcore/agent.md +6 -0
  53. package/kits/docs/evolcore/group-collaboration.md +251 -0
  54. package/kits/docs/evolcore/group-rules.md +1 -19
  55. package/kits/docs/evolcore/group.md +3 -1
  56. package/kits/docs/evolcore/trigger.md +6 -3
  57. package/kits/docs/prompt-loading-architecture.md +6 -0
  58. package/kits/eck_message_manifest.json +6 -6
  59. package/kits/schemas/_meta.json +3 -2
  60. package/kits/schemas/agent-config.schema.12.json +427 -0
  61. package/kits/templates/message-fragments/item.md +1 -1
  62. package/kits/templates/system-fragments/bootstrap.md +2 -1
  63. package/kits/templates/system-fragments/commands.md +2 -2
  64. 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
- if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
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 false;
485
+ return reclaimCorruptLock(lockDir);
482
486
  }
483
487
  if (!Number.isInteger(pid) || pid <= 0)
484
- return false;
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
- callId: input.requestId,
101
- correlationId: input.requestId,
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
- callId: input.correlationId ?? input.requestId,
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: event.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
- const text = this.renderWelcome({
170
- agentAid: aid,
171
- agentName,
172
- ownerName: ctx.recipientName || recipientId,
173
- channel: channelType || ctx.channelKey,
174
- baseagent,
175
- });
176
- await ctx.adapter.send(buildEnvelope({
177
- taskId: `bootstrap-${randomBytes(5).toString('hex')}`,
178
- channel: ctx.adapter.channelKey || ctx.adapter.channelName,
179
- channelId,
180
- agentName: aid,
181
- replyContext: {
182
- delivery,
183
- metadata: {
184
- source: 'daemon',
185
- persistRequired: true,
186
- operationId: bootstrapInitialMessageOperationId(aid),
187
- outboxTtl: BOOTSTRAP_MESSAGE_TTL_MS,
188
- criticalDelivery: true,
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
- }), { kind: 'result.text', text, isFinal: true });
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 : {};