evolcore 0.0.9 → 0.0.10

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 CHANGED
@@ -3,6 +3,13 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.10 (2026-08-06)
7
+
8
+ ### 数据迁移
9
+
10
+ - 修复联系人审计在多 Agent 场景下重复迁移的问题,审计记录改为写入各 Agent 自身数据目录。
11
+ - 迁移支持增量合并并复用已完成的迁移证明,同时校验来源内容变化,避免内容已改动时被错误跳过。
12
+
6
13
  ## 0.0.9 (2026-08-06)
7
14
 
8
15
  ### 安装与自启
@@ -566,7 +566,7 @@ function journalFile(selfAid) {
566
566
  return path.join(agentDir(selfAid), JOURNAL_NAME);
567
567
  }
568
568
  function appendAudit(selfAid, request, previousContactRevision, contactRevisionValue) {
569
- const file = path.join(resolvePaths().dataDir, 'contact-book-audit.jsonl');
569
+ const file = path.join(agentDir(selfAid), 'data', 'contact-audit.jsonl');
570
570
  fs.mkdirSync(path.dirname(file), { recursive: true });
571
571
  fs.appendFileSync(file, `${JSON.stringify({
572
572
  timestamp: new Date().toISOString(),
@@ -54,6 +54,14 @@ function operationTopologyKey(operation) {
54
54
  sessionChannelKey: operation.sessionChannelKey,
55
55
  });
56
56
  }
57
+ function operationProofTopologyKey(operation) {
58
+ if (operation.kind !== 'contact-audit-partition')
59
+ return operationTopologyKey(operation);
60
+ return JSON.stringify({
61
+ kind: operation.kind,
62
+ source: operation.source,
63
+ });
64
+ }
57
65
  /**
58
66
  * Manifests written before allowExistingDestinationMerge was introduced still
59
67
  * need safe recovery. Re-derive that narrow permission from their immutable
@@ -125,26 +133,40 @@ function acknowledgeCompletedCopies(root, operations) {
125
133
  if (manifest.planSignature === currentPlanSignature)
126
134
  return;
127
135
  for (const operation of manifest.operations ?? []) {
128
- if (operation.kind !== 'copy' || (operation.status !== 'committed' && operation.status !== 'skipped') || !operation.sourceHash)
136
+ if ((operation.kind !== 'copy' && operation.kind !== 'contact-audit-partition')
137
+ || (operation.status !== 'committed' && operation.status !== 'skipped')
138
+ || !operation.sourceHash)
129
139
  continue;
130
- const key = operationTopologyKey(operation);
131
- if (!proofs.has(key))
132
- proofs.set(key, { migrationId: manifest.id, sourceHash: operation.sourceHash });
140
+ const key = operationProofTopologyKey(operation);
141
+ if (!proofs.has(key)) {
142
+ proofs.set(key, {
143
+ migrationId: manifest.id,
144
+ sourceHash: operation.sourceHash,
145
+ destinations: operation.kind === 'contact-audit-partition'
146
+ ? [...(operation.destinations ?? [])]
147
+ : operation.destination ? [operation.destination] : [],
148
+ });
149
+ }
133
150
  }
134
151
  }
135
152
  }
136
153
  catch { }
137
154
  for (const operation of operations) {
138
- if (operation.kind !== 'copy' || !operation.destination || !fs.existsSync(operation.destination))
155
+ if (operation.kind !== 'copy' && operation.kind !== 'contact-audit-partition')
139
156
  continue;
140
- const proof = proofs.get(operationTopologyKey(operation));
157
+ const proof = proofs.get(operationProofTopologyKey(operation));
141
158
  if (!proof)
142
159
  continue;
160
+ if (operation.kind === 'copy' && (!operation.destination || !fs.existsSync(operation.destination)))
161
+ continue;
162
+ if (operation.kind === 'contact-audit-partition' && proof.destinations.some(destination => !fs.existsSync(destination)))
163
+ continue;
143
164
  try {
144
165
  if (treeHash(operation.source, operation.exclude) !== proof.sourceHash)
145
166
  continue;
146
167
  operation.status = 'acknowledged';
147
168
  operation.acknowledgedBy = proof.migrationId;
169
+ operation.sourceHash = proof.sourceHash;
148
170
  }
149
171
  catch { }
150
172
  }
@@ -1419,6 +1441,18 @@ function atomicWriteText(target, content) {
1419
1441
  function hashContent(content) {
1420
1442
  return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
1421
1443
  }
1444
+ function mergeJsonlContent(existing, incoming, deduplicateExisting = false) {
1445
+ const existingLines = existing.split('\n').filter(line => line.trim());
1446
+ const merged = deduplicateExisting ? [...new Set(existingLines)] : [...existingLines];
1447
+ const lines = new Set(merged);
1448
+ for (const line of incoming.split('\n')) {
1449
+ if (!line.trim() || lines.has(line))
1450
+ continue;
1451
+ lines.add(line);
1452
+ merged.push(line);
1453
+ }
1454
+ return merged.length ? `${merged.join('\n')}\n` : '';
1455
+ }
1422
1456
  function appendIssue(operation, issue) {
1423
1457
  const issues = operation.issues ?? [];
1424
1458
  if (!issues.includes(issue))
@@ -1457,20 +1491,18 @@ function executeContactAudit(manifest, operation, checkpoint) {
1457
1491
  for (const [aid, lines] of groups) {
1458
1492
  const destination = path.join(manifest.root, 'agents', aid, 'data', 'contact-audit.jsonl');
1459
1493
  const content = lines.join('\n') + '\n';
1460
- const expectedHash = hashContent(content);
1494
+ const existing = fs.existsSync(destination) ? fs.readFileSync(destination, 'utf8') : undefined;
1495
+ if (existing !== undefined)
1496
+ strictJsonValidation(destination);
1497
+ const expectedContent = existing === undefined ? content : mergeJsonlContent(existing, content);
1498
+ const expectedHash = hashContent(expectedContent);
1461
1499
  operation.destinations.push(destination);
1462
1500
  operation.destinationHashes[destination] = expectedHash;
1463
- if (fs.existsSync(destination)) {
1464
- const existing = fs.readFileSync(destination, 'utf8');
1501
+ if (existing !== undefined) {
1465
1502
  if (hashContent(existing) !== expectedHash) {
1466
- // Audit logs are append-only. An exact existing prefix is a partial
1467
- // prior copy, so replacing it with the verified complete partition is
1468
- // lossless; every other mismatch remains a manual conflict.
1469
- if (!content.startsWith(existing)) {
1470
- appendIssue(operation, `destination conflict: ${destination}`);
1471
- continue;
1472
- }
1473
- atomicWriteText(destination, content);
1503
+ // The per-Agent audit may have advanced after an earlier migration.
1504
+ // Preserve its order and append only legacy records not already present.
1505
+ atomicWriteText(destination, expectedContent);
1474
1506
  }
1475
1507
  continue;
1476
1508
  }
@@ -1600,15 +1632,7 @@ function trackMergeSources(operation) {
1600
1632
  return sources;
1601
1633
  }
1602
1634
  function mergeJsonl(target, source) {
1603
- const lines = new Set(fs.readFileSync(target, 'utf8').split('\n').filter(line => line.trim()));
1604
- const merged = [...lines];
1605
- for (const line of fs.readFileSync(source, 'utf8').split('\n')) {
1606
- if (!line.trim() || lines.has(line))
1607
- continue;
1608
- lines.add(line);
1609
- merged.push(line);
1610
- }
1611
- fs.writeFileSync(target, merged.length ? `${merged.join('\n')}\n` : '');
1635
+ fs.writeFileSync(target, mergeJsonlContent(fs.readFileSync(target, 'utf8'), fs.readFileSync(source, 'utf8'), true));
1612
1636
  }
1613
1637
  function copyTree(source, destination) {
1614
1638
  const stat = fs.statSync(source);
@@ -1741,6 +1765,34 @@ export function readDataMigration(root, id) {
1741
1765
  throw new Error(`migration manifest not found: ${id}`);
1742
1766
  return manifest;
1743
1767
  }
1768
+ function completedMigrationMatchesPlan(manifest, plan, planSignature) {
1769
+ if (manifest.state !== 'completed' || manifest.planSignature !== planSignature)
1770
+ return false;
1771
+ const proofs = new Map(manifest.operations.map(operation => [operationProofTopologyKey(operation), operation]));
1772
+ for (const operation of plan.operations.filter(item => item.status !== 'acknowledged')) {
1773
+ const proof = proofs.get(operationProofTopologyKey(operation));
1774
+ if (!proof || (proof.status !== 'committed' && proof.status !== 'skipped' && proof.status !== 'acknowledged'))
1775
+ return false;
1776
+ try {
1777
+ for (const source of operationSources(operation)) {
1778
+ const expected = proof.sourceHashes?.[source]
1779
+ ?? (source === proof.source ? proof.sourceHash : undefined);
1780
+ if (!expected || treeHash(source, operation.exclude) !== expected)
1781
+ return false;
1782
+ }
1783
+ }
1784
+ catch {
1785
+ return false;
1786
+ }
1787
+ if ((operation.kind === 'copy' || operation.kind === 'merge-copy')
1788
+ && (!operation.destination || !fs.existsSync(operation.destination)))
1789
+ return false;
1790
+ if (operation.kind === 'contact-audit-partition'
1791
+ && (proof.destinations ?? []).some(destination => !fs.existsSync(destination)))
1792
+ return false;
1793
+ }
1794
+ return true;
1795
+ }
1744
1796
  /**
1745
1797
  * Startup gate for the explicit migration workflow. Legacy source files are
1746
1798
  * deliberately retained after success, so their mere presence is not enough
@@ -1763,7 +1815,7 @@ export function inspectDataMigrationRequirement(root) {
1763
1815
  if (!entry.isFile() || !entry.name.endsWith('.json'))
1764
1816
  continue;
1765
1817
  const manifest = readJson(path.join(migrationDir(root), entry.name));
1766
- if (manifest?.state !== 'completed' || manifest.planSignature !== planSignature)
1818
+ if (!manifest || !completedMigrationMatchesPlan(manifest, plan, planSignature))
1767
1819
  continue;
1768
1820
  completedMigrationId = manifest.id;
1769
1821
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolcore",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",