evolcore 0.0.27 → 0.0.29-test.0

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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -3,22 +3,72 @@ import path from 'path';
3
3
  import { spawn, execFile } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../paths.js';
6
- import { loadDaemonConfig } from '../config-store.js';
6
+ import { isRestartRecoveryEnabled, loadDaemonConfig } from '../config-store.js';
7
7
  import * as platform from '../utils/cross-platform.js';
8
8
  import { EventBus } from '../core/event-bus.js';
9
- import { tryUpgrade, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
9
+ import { applyCachedPackageUpgrades, recordPackageUpgrade } from '../utils/package-version-info.js';
10
10
  import { isValidAid } from '../aun/aid/index.js';
11
11
  import { msgSend } from '../aun/msg/index.js';
12
12
  import { scanInstances, cleanupInstances, writeRestartMonitor, removeRestartMonitor, isRestartMonitorWinner, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
13
- import { WEB_PACKAGE_NAME } from '../product.js';
13
+ import { MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME } from '../product.js';
14
14
  import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
15
15
  import { rotateStdoutLog } from '../utils/log-writer.js';
16
- import { inspectDataMigrationRequirement } from '../core/data-migration.js';
17
- import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown } from './daemon-commands.js';
16
+ import { applyDataMigration, finalizeDataMigration, planDataMigration, verifyDataMigration, } from '../core/data-migration.js';
17
+ import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown, stopCodexAppServerOrphans } from './daemon-commands.js';
18
18
  import { getProcessStartTime, startTimeMatches } from '../utils/process-introspect.js';
19
19
  const execFileAsync = promisify(execFile);
20
+ /** Read-only check performed before stopping the current daemon. */
21
+ function preflightDataMigration(root, log) {
22
+ try {
23
+ const plan = planDataMigration(root);
24
+ const operations = plan.operations.filter(operation => operation.status !== 'acknowledged').length;
25
+ log(`Data migration preflight passed (${operations} pending operation(s))`);
26
+ for (const warning of plan.warnings)
27
+ log(`Migration preflight warning: ${warning}`);
28
+ return { ok: true, operations };
29
+ }
30
+ catch (error) {
31
+ const reason = error instanceof Error ? error.message : String(error);
32
+ log(`Data migration preflight failed: ${reason}`);
33
+ return { ok: false, reason };
34
+ }
35
+ }
36
+ /** Execute the migration transaction while the daemon is stopped. */
37
+ function applyPendingDataMigration(root, log) {
38
+ try {
39
+ const plan = planDataMigration(root);
40
+ const pending = plan.operations.filter(operation => operation.status !== 'acknowledged');
41
+ if (pending.length === 0) {
42
+ log('Data migration not required after upgrade');
43
+ return { ok: true, operations: 0 };
44
+ }
45
+ log(`Applying data migration (${pending.length} operation(s))`);
46
+ const applied = applyDataMigration(plan);
47
+ const verified = verifyDataMigration(applied, { strictTargetContent: true });
48
+ const finalization = finalizeDataMigration(root, verified);
49
+ if (verified.state !== 'completed') {
50
+ const failed = verified.operations.filter(operation => operation.status === 'error' || operation.issues?.length);
51
+ throw new Error(`migration verification failed (${failed.length} operation(s)); completed sources archived: ${finalization.archivePath}`);
52
+ }
53
+ log(`Data migration completed and legacy sources archived: ${finalization.archivePath}`);
54
+ return { ok: true, operations: pending.length };
55
+ }
56
+ catch (error) {
57
+ const reason = error instanceof Error ? error.message : String(error);
58
+ log(`Data migration failed: ${reason}`);
59
+ return { ok: false, reason };
60
+ }
61
+ }
20
62
  // Bound network notifications so they cannot hold up recovery after shutdown.
21
63
  const OWNER_NOTIFICATION_TIMEOUT_MS = 5000;
64
+ let foregroundDaemonExit;
65
+ async function finishSuccessfulRestart() {
66
+ if (foregroundDaemonExit) {
67
+ removeRestartMonitor();
68
+ process.exit(await foregroundDaemonExit);
69
+ }
70
+ process.exit(0);
71
+ }
22
72
  /**
23
73
  * The package this monitor operates on, which is not always the one it runs
24
74
  * from.
@@ -118,9 +168,9 @@ export async function cmdRestartMonitor() {
118
168
  const pendingFile = path.join(daemonControlDir(), 'restart-pending.json');
119
169
  // 升级成功时记下版本变化,回执里要告诉 agent 实际跑的是哪个版本。
120
170
  let upgradeVersions;
121
- const migrationRequirement = inspectDataMigrationRequirement(p.root);
122
- if (migrationRequirement.required) {
123
- const message = `Pending user-data migration (${migrationRequirement.operationCount} operations); leaving the current service running. Run ec data migrate --dry-run, then ec data migrate --apply.`;
171
+ const migrationPreflight = preflightDataMigration(p.root, log);
172
+ if (!migrationPreflight.ok) {
173
+ const message = `重启前用户数据迁移预检失败:${migrationPreflight.reason ?? 'unknown error'}`;
124
174
  log(message);
125
175
  console.error(`❌ ${message}`);
126
176
  cleanupPendingFile(pendingFile, log);
@@ -144,13 +194,14 @@ export async function cmdRestartMonitor() {
144
194
  // still alive. Refuse to create a second daemon when an unregistered process
145
195
  // from this package is found; a duplicate would connect AUN and kick the
146
196
  // existing session before failing on IPC/port ownership.
147
- const runtimeOrphans = orphanSnapshot.filter(o => {
197
+ const belongsToRuntime = (o) => {
148
198
  if (o.confirmedTestDaemon)
149
199
  return false;
150
200
  const homeMatches = o.evolcoreHome && normalizePath(o.evolcoreHome) === normalizePath(p.root);
151
201
  const packageMatches = !o.evolcoreHome && normalizePath(o.cmdline).includes(normalizePath(targetPackageRoot()));
152
202
  return Boolean(homeMatches || packageMatches);
153
- });
203
+ };
204
+ const runtimeOrphans = orphanSnapshot.filter(belongsToRuntime);
154
205
  if (runtimeOrphans.length > 0) {
155
206
  const pids = runtimeOrphans.map(o => o.pid).join(', ');
156
207
  log(`Stopping unregistered EvolCore process(es): ${pids}`);
@@ -238,15 +289,32 @@ export async function cmdRestartMonitor() {
238
289
  // for instance-file cleanup.
239
290
  cleanupInstances();
240
291
  }
241
- // 版本检查与自动升级
242
- log('Checking for updates...');
292
+ // Preserve CLI restart's final ownership guards and optional orphan cleanup
293
+ // here, after election/shutdown, so no launcher stops a daemon before a
294
+ // watchdog has successfully loaded.
295
+ const remaining = findOrphanProcesses();
296
+ if (scanInstances().mains.some(item => item.alive) || remaining.some(belongsToRuntime)) {
297
+ log('Restart aborted: a runtime process appeared or survived shutdown');
298
+ cleanupPendingFile(pendingFile, log);
299
+ process.exit(1);
300
+ }
301
+ const otherHome = remaining.filter(o => !o.confirmedTestDaemon && !belongsToRuntime(o));
302
+ if (process.argv.includes('--clear') && otherHome.length) {
303
+ log(`Cleaned cross-HOME orphans: ${killOrphans(otherHome).join(', ')}`);
304
+ }
305
+ else if (otherHome.length) {
306
+ log(`Other HOME orphan processes left running: ${otherHome.map(o => o.pid).join(', ')}`);
307
+ }
308
+ const stoppedCodex = stopCodexAppServerOrphans();
309
+ if (stoppedCodex)
310
+ log(`Cleaned ${stoppedCodex} EvolCore codex app-server orphan(s)`);
311
+ // Apply only the preflight cache. There is no registry VERSION query after
312
+ // shutdown (npm still downloads the exact cached package when needed).
313
+ log('Applying cached version information...');
243
314
  // 升级会原地替换整包,先留一个可回退的产物副本。备份放 EVOLCORE_HOME 而非包目录:
244
315
  // Linux/macOS 的全局目录常常不可写,而 HOME 一定可写。
245
316
  const upgradeBackup = await backupDistForUpgrade(p, log);
246
- const [upgrade, ecwebUpgrade] = await Promise.all([
247
- tryUpgrade(),
248
- tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME),
249
- ]);
317
+ const [upgrade, ecwebUpgrade] = await applyCachedPackageUpgrades(undefined, { log });
250
318
  let upgradeRolledBack;
251
319
  switch (upgrade.status) {
252
320
  case 'upgraded':
@@ -287,8 +355,7 @@ export async function cmdRestartMonitor() {
287
355
  await notifyDaemonOwners(p, `⚠️ evolcore 升级失败,使用当前版本继续\n${upgrade.error ?? ''}`.trim(), log);
288
356
  break;
289
357
  }
290
- // EC Web 是独立全局包,已安装时单独检查升级;与 EvolCore registry
291
- // 查询并行执行,npm 安装阶段由 npm-ops 的全局锁串行化。
358
+ // EC Web independently applies a valid cached result under the install lock.
292
359
  switch (ecwebUpgrade.status) {
293
360
  case 'upgraded':
294
361
  log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
@@ -316,12 +383,26 @@ export async function cmdRestartMonitor() {
316
383
  log(`↩ ${buildOutcome.reason ?? '已回滚'};继续以上一版本启动`);
317
384
  await notifyDaemonOwners(p, `⚠️ ${buildOutcome.reason ?? '已回滚'},正在以上一版本启动服务`, log);
318
385
  }
386
+ const migration = applyPendingDataMigration(p.root, log);
387
+ if (!migration.ok) {
388
+ const message = `服务未重启:用户数据迁移失败(${migration.reason ?? 'unknown error'})`;
389
+ markRestartOutcome(pendingFile, 'migration-failed', migration.reason, log);
390
+ cleanupPendingFile(pendingFile, log);
391
+ process.exit(1);
392
+ }
319
393
  // ECWeb is owned by the daemon. Starting it here used to race with the
320
394
  // daemon's own launcher and ECWeb's stale-instance cleanup, which could
321
395
  // leave ServiceProxy pointed at a dead PID.
322
396
  // 启动并检测 ready signal
323
397
  let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
324
398
  if (started) {
399
+ if (upgrade.status === 'upgraded' || upgrade.status === 'failed') {
400
+ await recordPackageUpgrade(MAIN_PACKAGE_NAME, {
401
+ from: upgrade.from, to: upgrade.to, at: new Date().toISOString(),
402
+ status: upgradeRolledBack ? 'rolled-back' : upgrade.status === 'upgraded' ? 'succeeded' : 'failed',
403
+ error: upgradeRolledBack ?? upgrade.error,
404
+ }, log);
405
+ }
325
406
  if (upgradeRolledBack) {
326
407
  markRestartOutcome(pendingFile, 'rolled-back', upgradeRolledBack, log);
327
408
  log(`✓ Service restarted after upgrade rollback`);
@@ -341,15 +422,21 @@ export async function cmdRestartMonitor() {
341
422
  }
342
423
  archiveSelfHealLog(p, log);
343
424
  // 发起会话内的“重启成功”通知由新进程自行发送,此处只负责失败/自愈告警。
344
- process.exit(0);
425
+ await finishSuccessfulRestart();
345
426
  }
346
427
  // 升级后的新版本起不来:回退到升级前的产物再试一次。
347
428
  if (upgradeVersions?.to && !upgradeRolledBack) {
429
+ await recordPackageUpgrade(MAIN_PACKAGE_NAME, {
430
+ ...upgradeVersions, at: new Date().toISOString(), status: 'failed', error: 'Upgraded daemon failed its ready check',
431
+ }, log);
348
432
  if (await restoreDistBackup(upgradeBackup, log)) {
349
433
  await notifyDaemonOwners(p, `⚠️ 升级到 ${upgradeVersions.to} 后启动失败,正在回滚到 ${upgradeVersions.from} 重试...`, log);
350
434
  started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
351
435
  if (started) {
352
436
  const reason = `升级到 ${upgradeVersions.to} 后启动失败,已回滚到 ${upgradeVersions.from}`;
437
+ await recordPackageUpgrade(MAIN_PACKAGE_NAME, {
438
+ ...upgradeVersions, at: new Date().toISOString(), status: 'rolled-back', error: reason,
439
+ }, log);
353
440
  log(`✓ ${reason}`);
354
441
  markRestartOutcome(pendingFile, 'rolled-back', reason, log);
355
442
  await notifyDaemonOwners(p, `⚠️ ${reason}。当前运行旧版本,请检查后重试。`, log);
@@ -390,6 +477,13 @@ export async function cmdRestartMonitor() {
390
477
  cleanupPendingFile(pendingFile, log);
391
478
  process.exit(1);
392
479
  }
480
+ if (!isRestartRecoveryEnabled()) {
481
+ const message = '服务启动失败,restartRecovery.enabled 已关闭,已跳过 Claude 自动修复。';
482
+ log(`⚠️ ${message}`);
483
+ await notifyDaemonOwners(p, `⚠️ ${message}`, log);
484
+ cleanupPendingFile(pendingFile, log);
485
+ process.exit(1);
486
+ }
393
487
  // 启动失败,进入 self-heal 循环
394
488
  log('❌ Service failed to start, entering self-heal loop');
395
489
  eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
@@ -858,9 +952,10 @@ async function spawnAndWaitReady(p, log, timeout) {
858
952
  const out = fs.openSync(stdoutLog, 'a');
859
953
  const err = fs.openSync(stdoutLog, 'a');
860
954
  const appMain = path.join(targetPackageRoot(), 'dist', 'index.js');
955
+ const foreground = process.argv.includes('--foreground');
861
956
  const child = spawn(process.execPath, ['--no-warnings=ExperimentalWarning', appMain], {
862
- detached: true,
863
- stdio: ['ignore', out, err],
957
+ detached: !foreground,
958
+ stdio: foreground ? 'inherit' : ['ignore', out, err],
864
959
  windowsHide: true,
865
960
  env: {
866
961
  ...process.env,
@@ -872,7 +967,16 @@ async function spawnAndWaitReady(p, log, timeout) {
872
967
  }
873
968
  });
874
969
  const childPid = child.pid;
875
- child.unref();
970
+ if (foreground) {
971
+ foregroundDaemonExit = new Promise(resolve => {
972
+ child.once('error', () => resolve(1));
973
+ child.once('exit', code => resolve(code ?? 1));
974
+ });
975
+ }
976
+ else
977
+ child.unref();
978
+ fs.closeSync(out);
979
+ fs.closeSync(err);
876
980
  log(`Spawned new process PID: ${childPid}, waiting for ready signal...`);
877
981
  // 轮询等待 ready.signal 出现
878
982
  const start = Date.now();
@@ -927,7 +1031,7 @@ async function invokeClaude(claudePath, p, attempt, maxAttempts, timeout, log) {
927
1031
  - 检查是否有旧进程仍在运行:\`ps aux | grep 'node.*dist/index.js' | grep -v grep\`,旧进程可能占用端口或锁文件
928
1032
  - 可以运行 \`EVOLCORE_HOME=${p.root} node dist/cli/index.js diagnose\` 快速检查配置和数据库
929
1033
  - 如果进程无任何输出就 exit(1),说明是 process.exit(1) 被显式调用,搜索源码中所有 process.exit(1) 位置
930
- - 配置文件使用双 rename 原子写(foo.json → foo.json_ → foo.json__),崩溃时可从 foo.json_ 恢复
1034
+ - 配置文件先写入 foo.json__,再保留旧版本到 foo.json_,最后原子切换为 foo.json;崩溃后会丢弃未完成的 foo.json__,仅在 foo.json 缺失时从 foo.json_ 恢复
931
1035
 
932
1036
  请执行以下步骤:
933
1037
  1. 读取 ${stdoutLog} 和 ${p.logs}/daemon-*.log(最新文件)的最后 50 行
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `ec skill` 命令。
3
+ *
4
+ * 这是一层薄转发:真正的实现是内置技能 ec-skill 自己的脚本
5
+ * (`kits/skills/ec-skill/scripts/cli.mjs`)。CLI 不复制逻辑,
6
+ * 模型侧直接跑同一份脚本,两边行为天然一致。
7
+ *
8
+ * 用子进程而非 import,理由:脚本是 ESM、位于包的 kits/ 下,
9
+ * 而 kits/ 随包发布但不参与 tsc 编译;用 spawn 最直接,
10
+ * 也保留了脚本「自定位、不依赖 cwd」的约束。
11
+ *
12
+ * 权限门禁同样在脚本层的 skill.mjs `assertPermission()`,本文件不复制一份。
13
+ * 两个入口(这里的 spawn 与模型侧直接 node脚本)环境变量一致,
14
+ * 脚本自己读 EVOLCORE_TASK_RUNTIME_CONTEXT 即可得到相同判据;
15
+ * 若在 CLI 层再判一遍,只是多一处会与脚本漂移的实现。
16
+ */
17
+ import path from 'path';
18
+ import { spawn } from 'child_process';
19
+ import { getPackageRoot, agentDir } from '../paths.js';
20
+ import { readTaskRuntimeContextFromEnv } from './task-context.js';
21
+ export async function cmdSkill(args) {
22
+ const script = path.join(getPackageRoot(), 'kits', 'skills', 'ec-skill', 'scripts', 'cli.mjs');
23
+ // 脚本在包外运行,拿不到主程序的路径工具,这里把 Agent 目录显式传进去,
24
+ // 让它能定位 Agent 级 skills/ 与 .switches.json。
25
+ // selfAid 优先取任务运行时上下文(托管会话的权威来源),其次环境变量。
26
+ const env = { ...process.env };
27
+ const selfAid = readTaskRuntimeContextFromEnv()?.selfAid || process.env.EVOLCORE_SELF_AID;
28
+ if (selfAid && !env.EVOLCORE_AGENT_DIR) {
29
+ env.EVOLCORE_AGENT_DIR = agentDir(selfAid);
30
+ }
31
+ const code = await new Promise((resolve, reject) => {
32
+ const child = spawn(process.execPath, [script, ...args], {
33
+ stdio: 'inherit',
34
+ env,
35
+ });
36
+ child.on('error', reject);
37
+ child.on('exit', (c) => resolve(c ?? 0));
38
+ });
39
+ if (code !== 0)
40
+ process.exitCode = code;
41
+ }
@@ -34,6 +34,7 @@ function normalizeTaskRuntimeContext(value) {
34
34
  chatType: optionalString(value.chatType),
35
35
  selfAid: optionalString(value.selfAid),
36
36
  peerId: optionalString(value.peerId),
37
+ peerKey: optionalString(value.peerKey),
37
38
  peerName: optionalString(value.peerName),
38
39
  peerType: optionalString(value.peerType),
39
40
  peerRole: optionalString(value.peerRole),
@@ -51,6 +52,7 @@ function normalizeTaskRuntimeContext(value) {
51
52
  runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
52
53
  handoffIds: handoffIds?.length ? handoffIds : undefined,
53
54
  causation: normalizeCausation(value.causation),
55
+ consolidationBatchDir: optionalString(value.consolidationBatchDir ?? value.consolidationLogBase),
54
56
  };
55
57
  }
56
58
  function optionalProcessRole(value) {
@@ -60,29 +62,6 @@ function optionalProcessRole(value) {
60
62
  function optionalDataScope(value) {
61
63
  return value === 'relation' || value === 'agent' || value === 'daemon' ? value : undefined;
62
64
  }
63
- /**
64
- * Codex's shell carrier can leave one JSON-style escape layer in a message
65
- * argument (for example, the two characters `\\` and `n`). Decode only that
66
- * layer, and only for a task that is explicitly running the Codex backend.
67
- * Actual line breaks and all non-Codex CLI input remain unchanged.
68
- */
69
- export function decodeCodexMessageText(text, runtime) {
70
- if (runtime?.baseagent?.trim().toLowerCase() !== 'codex')
71
- return text;
72
- return text.replace(/\\([nrt])/g, (_match, escape) => {
73
- if (escape === 'n')
74
- return '\n';
75
- if (escape === 'r')
76
- return '\r';
77
- return '\t';
78
- });
79
- }
80
- /** Apply the Codex-only normalization to a text message payload. */
81
- export function normalizeCodexTextPayload(payload, runtime) {
82
- if (payload.type !== 'text' || typeof payload.text !== 'string')
83
- return;
84
- payload.text = decodeCodexMessageText(payload.text, runtime);
85
- }
86
65
  function optionalAbsolutePath(value) {
87
66
  if (typeof value !== 'string' || !path.isAbsolute(value))
88
67
  return undefined;
@@ -5,9 +5,17 @@ import { ipcQuery } from '../ipc.js';
5
5
  import { isHelpFlag, wantsHelp } from './help.js';
6
6
  import { readDelegationCredential } from '../core/auth/agent-delegation.js';
7
7
  import { readTaskRuntimeContextFromEnv } from './task-context.js';
8
+ import { assertEcSingleLine } from './cli-argv.js';
9
+ import { canonicalizeEvolcoreCommandArgv } from '../core/permission/ec-command-parser.js';
8
10
  import { normalizeTriggerDefinition, resolveScriptPath, } from '../trigger/validation.js';
9
11
  export async function cmdTrigger(args) {
12
+ args = canonicalizeEvolcoreCommandArgv(['ec', 'trigger', ...args]).slice(2);
10
13
  const sub = args[0];
14
+ if (sub === 'create' || sub === 'set' || sub === 'update') {
15
+ const nameIndex = args.indexOf('--name');
16
+ if (nameIndex >= 0)
17
+ assertEcSingleLine(args[nameIndex + 1], '--name');
18
+ }
11
19
  if (!sub || isHelpFlag(sub)) {
12
20
  printHelp();
13
21
  return;
@@ -112,7 +120,7 @@ Create 参数模式支持:
112
120
  --execution-timeout <时长>(模型类 Trigger 单次执行总时限)
113
121
  --model <模型> --effort <low|medium|high|xhigh|max>
114
122
  --max-runs <次数> --max-duration <时长: 30s|15m|2h|1d>
115
- --permission <readonly|auto|request|bypass|fullaccess>(省略则继承当前身份配置;fullaccess 仅 daemon-owner 且 feature flag 开启时可用)
123
+ --permission <readonly|auto|request|bypass|fullaccess>(省略则继承当前身份配置;fullaccess 仅 daemon-owner 可用)
116
124
  --tz <时区> (仅 cron)
117
125
  --target-channel <channelKey> --target-channel-id <ID> (AUN 可简写为 aun)
118
126
  --target-chat-type <private|group> (AUN 目标必填;channelId 同时作为群聊 ID)
@@ -0,0 +1,209 @@
1
+ /**
2
+ * ec watch diag — 消息生命周期诊断
3
+ * 固定N行,每行一个环节,原地刷新,显示该环节的计数 + 最后处理的消息。
4
+ * 数据源:各处理环节直接打点写入的 msg-diag.jsonl,不分析其他日志。
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { resolvePaths } from '../paths.js';
10
+ // ==================== ANSI ====================
11
+ const isTTY = !!process.stdout.isTTY;
12
+ const RST = isTTY ? '\x1b[0m' : '';
13
+ const DIM = isTTY ? '\x1b[2m' : '';
14
+ const BOLD = isTTY ? '\x1b[1m' : '';
15
+ const CYAN = isTTY ? '\x1b[36m' : '';
16
+ const GREEN = isTTY ? '\x1b[32m' : '';
17
+ const YELLOW = isTTY ? '\x1b[33m' : '';
18
+ const BLUE = isTTY ? '\x1b[34m' : '';
19
+ const GRAY = isTTY ? '\x1b[38;5;244m' : '';
20
+ const UP = (n) => isTTY ? `\x1b[${n}A` : '';
21
+ const CLR = isTTY ? '\x1b[2K\r' : '';
22
+ // ==================== Stage definitions ====================
23
+ const STAGES = [
24
+ { stage: 'sdk_received', label: '0. SDK收到 ', color: GRAY },
25
+ { stage: 'aun_in', label: '1. AUN入站 ', color: GREEN },
26
+ { stage: 'queue_enqueue', label: '2. 队列入队 ', color: CYAN },
27
+ { stage: 'task_created', label: '3. 任务创建 ', color: YELLOW },
28
+ { stage: 'task_start', label: '4. 任务开始 ', color: YELLOW },
29
+ { stage: 'task_done', label: '5. 任务完成 ', color: BLUE },
30
+ { stage: 'aun_out', label: '6. AUN出站 ', color: GREEN },
31
+ ];
32
+ const state = new Map();
33
+ for (const s of STAGES)
34
+ state.set(s.stage, { count: 0 });
35
+ // ==================== Helpers ====================
36
+ function hhmmssms(iso) {
37
+ const d = new Date(iso);
38
+ return (String(d.getHours()).padStart(2, '0') + ':' +
39
+ String(d.getMinutes()).padStart(2, '0') + ':' +
40
+ String(d.getSeconds()).padStart(2, '0') + '.' +
41
+ String(d.getMilliseconds()).padStart(3, '0'));
42
+ }
43
+ function cut(s, n) {
44
+ if (!s)
45
+ return '';
46
+ return s.length <= n ? s : s.slice(0, n - 1) + '…';
47
+ }
48
+ function pad(s, n) {
49
+ return s.length >= n ? s : s + ' '.repeat(n - s.length);
50
+ }
51
+ // ==================== Render ====================
52
+ let firstRender = true;
53
+ const HEADER_LINES = 3;
54
+ function renderHeader() {
55
+ const cols = process.stdout.columns ?? 120;
56
+ let buf = '';
57
+ buf += `${BOLD}${CYAN}━━━ ec watch diag — 消息生命周期诊断 ━━━${RST}\n`;
58
+ buf += `${DIM}各环节实时显示处理计数和最后一条消息,Ctrl+C 退出${RST}\n`;
59
+ buf += `${GRAY}${'─'.repeat(Math.min(cols, 120))}${RST}\n`;
60
+ return buf;
61
+ }
62
+ function renderRow(stageDef, st) {
63
+ const countStr = `[${String(st.count).padStart(4)}]`;
64
+ const label = `${stageDef.color}${stageDef.label}${RST}`;
65
+ if (!st.last) {
66
+ return `${CLR}${label} ${DIM}${countStr}${RST} ${GRAY}(等待中...)${RST}`;
67
+ }
68
+ const e = st.last;
69
+ const time = hhmmssms(e.ts);
70
+ const from = cut(e.from || e.aid, 20);
71
+ const to = cut(e.to, 20);
72
+ const arrow = from && to ? `${from}${GRAY}→${RST}${to}` : (from || to || `${GRAY}---${RST}`);
73
+ const preview = e.preview ? `${DIM}"${cut(e.preview, 40)}"${RST}` : '';
74
+ const taskStr = e.taskId ? `${GRAY}[${cut(e.taskId, 22)}]${RST} ` : '';
75
+ const sessStr = e.sessionId ? `${GRAY}sess=${cut(e.sessionId, 18)}${RST} ` : '';
76
+ return `${CLR}${label} ${DIM}${countStr}${RST} ${GRAY}${time}${RST} ${pad(arrow, 42)} ${taskStr}${sessStr}${preview}`;
77
+ }
78
+ function render(_redrawAll) {
79
+ if (!isTTY)
80
+ return;
81
+ // 每次都清屏重绘,彻底避免因内容长度不一导致的行偏移问题
82
+ let buf = '\x1b[2J\x1b[H';
83
+ buf += `${BOLD}${CYAN}━━━ ec watch diag — 消息生命周期诊断 ━━━${RST}\n`;
84
+ const cols = process.stdout.columns ?? 120;
85
+ buf += `${DIM}各环节实时显示处理计数和最后一条消息,Ctrl+C 退出${RST}\n`;
86
+ buf += `${GRAY}${'─'.repeat(Math.min(cols, 120))}${RST}\n`;
87
+ for (const s of STAGES) {
88
+ buf += renderRow(s, state.get(s.stage)) + '\n';
89
+ }
90
+ process.stdout.write(buf);
91
+ firstRender = false;
92
+ }
93
+ // 非 TTY 模式打印新事件
94
+ function printNonTty(entry) {
95
+ const stageDef = STAGES.find(s => s.stage === entry.stage);
96
+ if (!stageDef)
97
+ return;
98
+ const st = state.get(entry.stage);
99
+ const time = hhmmssms(entry.ts);
100
+ const from = cut(entry.from || entry.aid, 14);
101
+ const to = cut(entry.to, 14);
102
+ console.log(`${stageDef.label.trim()} [${st.count}] ${time} ${from}→${to} "${entry.preview}"`);
103
+ }
104
+ // ==================== Log tailing ====================
105
+ function getDiagLogPath() {
106
+ const p = resolvePaths();
107
+ // resolvePaths().root 是 ~/.evolcore
108
+ const home = process.env.EVOLCORE_HOME ?? path.join(os.homedir(), '.evolcore');
109
+ return path.join(home, 'logs', 'msg-diag.jsonl');
110
+ }
111
+ function processLine(line) {
112
+ if (!line.trim())
113
+ return false;
114
+ try {
115
+ const entry = JSON.parse(line);
116
+ if (!entry.stage || !STAGES.find(s => s.stage === entry.stage))
117
+ return false;
118
+ const st = state.get(entry.stage);
119
+ if (!st)
120
+ return false;
121
+ st.count++;
122
+ st.last = entry;
123
+ return true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ // ==================== Main ====================
130
+ export async function cmdWatchMsgDiag(_args) {
131
+ const logPath = getDiagLogPath();
132
+ if (!isTTY) {
133
+ // 非 TTY:只打印新事件
134
+ console.log('# ec watch diag — 消息生命周期诊断(非 TTY 模式)');
135
+ }
136
+ else {
137
+ process.stdout.write('\x1b[?25l'); // 隐藏光标
138
+ }
139
+ // 初始加载:读取已有历史(最后 50KB)
140
+ let filePos = 0;
141
+ if (fs.existsSync(logPath)) {
142
+ const stat = fs.statSync(logPath);
143
+ filePos = Math.max(0, stat.size - 50000);
144
+ const fd = fs.openSync(logPath, 'r');
145
+ const buf = Buffer.alloc(stat.size - filePos);
146
+ fs.readSync(fd, buf, 0, buf.length, filePos);
147
+ fs.closeSync(fd);
148
+ const lines = buf.toString('utf-8').split('\n');
149
+ for (const line of lines)
150
+ processLine(line);
151
+ filePos = stat.size;
152
+ }
153
+ // 初始渲染
154
+ render(true);
155
+ let curLogPath = logPath;
156
+ function tick() {
157
+ // 检查文件是否存在(daemon 重启后才会创建)
158
+ if (!fs.existsSync(curLogPath)) {
159
+ if (!firstRender) {
160
+ // 文件消失了,说明 daemon 重启,重置状态
161
+ for (const s of STAGES)
162
+ state.set(s.stage, { count: 0 });
163
+ filePos = 0;
164
+ render(true);
165
+ }
166
+ return;
167
+ }
168
+ try {
169
+ const stat = fs.statSync(curLogPath);
170
+ if (stat.size <= filePos)
171
+ return; // 无新内容
172
+ const fd = fs.openSync(curLogPath, 'r');
173
+ const buf = Buffer.alloc(stat.size - filePos);
174
+ fs.readSync(fd, buf, 0, buf.length, filePos);
175
+ fs.closeSync(fd);
176
+ filePos = stat.size;
177
+ const lines = buf.toString('utf-8').split('\n');
178
+ let changed = false;
179
+ for (const line of lines) {
180
+ if (processLine(line)) {
181
+ changed = true;
182
+ if (!isTTY) {
183
+ try {
184
+ const entry = JSON.parse(line);
185
+ printNonTty(entry);
186
+ }
187
+ catch { }
188
+ }
189
+ }
190
+ }
191
+ // 批量处理完所有新行后,只渲染一次
192
+ if (changed && isTTY)
193
+ render(false);
194
+ }
195
+ catch {
196
+ // 文件轮转中,忽略
197
+ }
198
+ }
199
+ const interval = setInterval(tick, 300);
200
+ process.on('SIGINT', () => {
201
+ clearInterval(interval);
202
+ if (isTTY) {
203
+ process.stdout.write('\x1b[?25h\n');
204
+ }
205
+ console.log('\n诊断监控已停止');
206
+ process.exit(0);
207
+ });
208
+ await new Promise(() => { });
209
+ }