evolcore 0.0.2 → 0.0.4
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 +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { isValidAid } from '../aun/aid/validation.js';
|
|
5
|
+
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
6
|
+
import { agentConfig, agentContactConfig, agentDir, resolvePaths } from '../paths.js';
|
|
7
|
+
import { atomicReadJson, atomicWrite, atomicWriteJson } from '../utils/atomic-write.js';
|
|
8
|
+
import { fileCache } from '../core/daemon-file-cache.js';
|
|
9
|
+
import { ConfigTarget, notifyConfigWrite, validateConfig } from './config-manager.js';
|
|
10
|
+
import { parseContactAlias } from './contact-alias.js';
|
|
11
|
+
import { clearRoleStoreCache } from './role-store.js';
|
|
12
|
+
const LOCK_NAME = '.contact-book-mutation.lock';
|
|
13
|
+
const JOURNAL_NAME = '.contact-book-mutation.json';
|
|
14
|
+
const LOCK_WAIT_MS = 5_000;
|
|
15
|
+
const LOCK_RETRY_MS = 20;
|
|
16
|
+
export const EMPTY_CONTACT_REVISION = crypto.createHash('sha256').update('').digest('hex');
|
|
17
|
+
export class ContactMutationError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
data;
|
|
20
|
+
constructor(code, message, data) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.data = data;
|
|
24
|
+
this.name = 'ContactMutationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const mutationTails = new Map();
|
|
28
|
+
export function contactRevision(config) {
|
|
29
|
+
if (!config)
|
|
30
|
+
return EMPTY_CONTACT_REVISION;
|
|
31
|
+
return crypto.createHash('sha256').update(stableStringify(config)).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
export function readContactRevision(selfAid) {
|
|
34
|
+
return contactRevision(readContactBookFromDisk(selfAid));
|
|
35
|
+
}
|
|
36
|
+
export function mutateContactBook(request) {
|
|
37
|
+
return enqueueContactMutation(request, []);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Prepare a mutation while holding the same cross-process lock used by every Contact Book write.
|
|
41
|
+
* The callback must stay synchronous so the lock is never retained across an await boundary.
|
|
42
|
+
*/
|
|
43
|
+
export function mutateContactBookPrepared(selfAid, prepare) {
|
|
44
|
+
const normalizedAid = normalizeAid(selfAid, 'self AID');
|
|
45
|
+
const previous = mutationTails.get(normalizedAid) ?? Promise.resolve();
|
|
46
|
+
let release;
|
|
47
|
+
const tail = new Promise(resolve => { release = resolve; });
|
|
48
|
+
mutationTails.set(normalizedAid, tail);
|
|
49
|
+
return previous
|
|
50
|
+
.catch(() => { })
|
|
51
|
+
.then(() => mutateContactBookPreparedSyncInternal(normalizedAid, prepare))
|
|
52
|
+
.finally(() => {
|
|
53
|
+
release();
|
|
54
|
+
if (mutationTails.get(normalizedAid) === tail)
|
|
55
|
+
mutationTails.delete(normalizedAid);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function mutateContactBookTransaction(request, changes) {
|
|
59
|
+
return enqueueContactMutation(request, changes);
|
|
60
|
+
}
|
|
61
|
+
/** Synchronous transaction variant for configuration writes that must commit atomically with contacts. */
|
|
62
|
+
export function mutateContactBookTransactionSync(request, changes) {
|
|
63
|
+
return mutateContactBookSyncInternal(request, changes);
|
|
64
|
+
}
|
|
65
|
+
function enqueueContactMutation(request, changes) {
|
|
66
|
+
const selfAid = normalizeAid(request.selfAid, 'self AID');
|
|
67
|
+
const previous = mutationTails.get(selfAid) ?? Promise.resolve();
|
|
68
|
+
let release;
|
|
69
|
+
const tail = new Promise(resolve => { release = resolve; });
|
|
70
|
+
mutationTails.set(selfAid, tail);
|
|
71
|
+
return previous
|
|
72
|
+
.catch(() => { })
|
|
73
|
+
.then(() => mutateContactBookSyncInternal({ ...request, selfAid }, changes))
|
|
74
|
+
.finally(() => {
|
|
75
|
+
release();
|
|
76
|
+
if (mutationTails.get(selfAid) === tail)
|
|
77
|
+
mutationTails.delete(selfAid);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Synchronous compatibility entry for channel bind flows. The critical section has no await. */
|
|
81
|
+
export function mutateContactBookSync(request) {
|
|
82
|
+
return mutateContactBookSyncInternal(request, []);
|
|
83
|
+
}
|
|
84
|
+
/** Recover every interrupted Contact transaction before startup reads config. */
|
|
85
|
+
export function recoverAllContactBookMutationsSync() {
|
|
86
|
+
const agentsRoot = resolvePaths().agentsDir;
|
|
87
|
+
if (!fs.existsSync(agentsRoot))
|
|
88
|
+
return 0;
|
|
89
|
+
let recovered = 0;
|
|
90
|
+
for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
91
|
+
if (!entry.isDirectory() || !isValidAid(entry.name))
|
|
92
|
+
continue;
|
|
93
|
+
if (!fs.existsSync(journalFile(entry.name)))
|
|
94
|
+
continue;
|
|
95
|
+
const release = acquireAgentLock(entry.name);
|
|
96
|
+
try {
|
|
97
|
+
if (recoverMutation(entry.name))
|
|
98
|
+
recovered += 1;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
release();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return recovered;
|
|
105
|
+
}
|
|
106
|
+
function mutateContactBookSyncInternal(request, transactionChanges) {
|
|
107
|
+
const selfAid = normalizeAid(request.selfAid, 'self AID');
|
|
108
|
+
const release = acquireAgentLock(selfAid);
|
|
109
|
+
try {
|
|
110
|
+
recoverMutation(selfAid);
|
|
111
|
+
const file = agentContactConfig(selfAid);
|
|
112
|
+
const current = readContactBookFromDisk(selfAid);
|
|
113
|
+
const previousRevision = contactRevision(current);
|
|
114
|
+
if (request.expectedContactRevision !== undefined
|
|
115
|
+
&& request.expectedContactRevision !== previousRevision) {
|
|
116
|
+
throw new ContactMutationError('CONFLICT', 'Contact book changed since it was read', {
|
|
117
|
+
expectedContactRevision: request.expectedContactRevision,
|
|
118
|
+
contactRevision: previousRevision,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const next = cloneBook(current);
|
|
122
|
+
applyMutation(next, request.mutation, selfAid);
|
|
123
|
+
validateContactBook(next, selfAid);
|
|
124
|
+
const nextRevision = contactRevision(next);
|
|
125
|
+
validateTransactionChanges(selfAid, transactionChanges);
|
|
126
|
+
if (nextRevision === previousRevision && transactionChanges.length === 0) {
|
|
127
|
+
return { changed: false, contactRevision: previousRevision, previousContactRevision: previousRevision, contact: next };
|
|
128
|
+
}
|
|
129
|
+
const before = readTextOrNull(file);
|
|
130
|
+
const after = `${JSON.stringify(next, null, 2)}\n`;
|
|
131
|
+
const changes = [
|
|
132
|
+
...(nextRevision === previousRevision ? [] : [{ file, before, after }]),
|
|
133
|
+
...transactionChanges,
|
|
134
|
+
];
|
|
135
|
+
writeJournal(selfAid, { schemaVersion: 1, state: 'prepared', selfAid, changes });
|
|
136
|
+
try {
|
|
137
|
+
for (const change of changes)
|
|
138
|
+
applyTransactionChange(change);
|
|
139
|
+
writeJournal(selfAid, { schemaVersion: 1, state: 'committed', selfAid, changes });
|
|
140
|
+
removeJournal(selfAid);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
recoverMutation(selfAid);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
if (nextRevision !== previousRevision) {
|
|
147
|
+
notifyConfigWrite(ConfigTarget.Contact, { self: selfAid });
|
|
148
|
+
appendAudit(selfAid, request, previousRevision, nextRevision);
|
|
149
|
+
}
|
|
150
|
+
for (const change of transactionChanges)
|
|
151
|
+
fileCache.invalidate(change.file);
|
|
152
|
+
if (transactionChanges.length)
|
|
153
|
+
clearRoleStoreCache(selfAid);
|
|
154
|
+
return {
|
|
155
|
+
changed: nextRevision !== previousRevision,
|
|
156
|
+
contactRevision: nextRevision,
|
|
157
|
+
previousContactRevision: previousRevision,
|
|
158
|
+
contact: next,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
release();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function mutateContactBookPreparedSyncInternal(selfAid, prepare) {
|
|
166
|
+
const release = acquireAgentLock(selfAid);
|
|
167
|
+
try {
|
|
168
|
+
recoverMutation(selfAid);
|
|
169
|
+
const file = agentContactConfig(selfAid);
|
|
170
|
+
const current = readContactBookFromDisk(selfAid);
|
|
171
|
+
const previousRevision = contactRevision(current);
|
|
172
|
+
const next = cloneBook(current);
|
|
173
|
+
const prepared = prepare({ contact: next, contactRevision: previousRevision });
|
|
174
|
+
if (prepared.mutation)
|
|
175
|
+
applyMutation(next, prepared.mutation, selfAid);
|
|
176
|
+
validateContactBook(next, selfAid);
|
|
177
|
+
const nextRevision = contactRevision(next);
|
|
178
|
+
if (nextRevision === previousRevision) {
|
|
179
|
+
return {
|
|
180
|
+
changed: false,
|
|
181
|
+
contactRevision: previousRevision,
|
|
182
|
+
previousContactRevision: previousRevision,
|
|
183
|
+
contact: next,
|
|
184
|
+
value: prepared.value,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
prepared.beforeCommit?.({
|
|
188
|
+
contact: next,
|
|
189
|
+
previousContactRevision: previousRevision,
|
|
190
|
+
contactRevision: nextRevision,
|
|
191
|
+
});
|
|
192
|
+
const before = readTextOrNull(file);
|
|
193
|
+
const after = `${JSON.stringify(next, null, 2)}\n`;
|
|
194
|
+
const changes = [{ file, before, after }];
|
|
195
|
+
writeJournal(selfAid, { schemaVersion: 1, state: 'prepared', selfAid, changes });
|
|
196
|
+
try {
|
|
197
|
+
applyTransactionChange(changes[0]);
|
|
198
|
+
writeJournal(selfAid, { schemaVersion: 1, state: 'committed', selfAid, changes });
|
|
199
|
+
removeJournal(selfAid);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
recoverMutation(selfAid);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
notifyConfigWrite(ConfigTarget.Contact, { self: selfAid });
|
|
206
|
+
appendAudit(selfAid, {
|
|
207
|
+
selfAid,
|
|
208
|
+
actor: prepared.actor,
|
|
209
|
+
mutation: prepared.mutation,
|
|
210
|
+
}, previousRevision, nextRevision);
|
|
211
|
+
return {
|
|
212
|
+
changed: true,
|
|
213
|
+
contactRevision: nextRevision,
|
|
214
|
+
previousContactRevision: previousRevision,
|
|
215
|
+
contact: next,
|
|
216
|
+
value: prepared.value,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
release();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function applyMutation(book, mutation, selfAid) {
|
|
224
|
+
const contacts = book.contacts;
|
|
225
|
+
if (mutation.type === 'batch') {
|
|
226
|
+
for (const item of mutation.mutations)
|
|
227
|
+
applyMutation(book, item, selfAid);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (mutation.type === 'ensure-contacts') {
|
|
231
|
+
for (const rawPrimaryId of mutation.primaryIds) {
|
|
232
|
+
const primaryId = normalizeContactAid(rawPrimaryId);
|
|
233
|
+
if (!contacts[primaryId])
|
|
234
|
+
contacts[primaryId] = {};
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const primaryId = normalizeContactAid(mutation.primaryId);
|
|
239
|
+
const existing = contacts[primaryId];
|
|
240
|
+
if (mutation.type === 'set-status') {
|
|
241
|
+
if (isOwner(selfAid, primaryId) && mutation.status === 'blocked') {
|
|
242
|
+
throw new ContactMutationError('OWNER_PROTECTED', 'An Agent owner cannot be blocked');
|
|
243
|
+
}
|
|
244
|
+
contacts[primaryId] = compactEntry({
|
|
245
|
+
...existing,
|
|
246
|
+
status: mutation.status === 'active' ? undefined : mutation.status,
|
|
247
|
+
pendingRequest: undefined,
|
|
248
|
+
declinedAt: mutation.status === 'declined' ? existing?.declinedAt ?? new Date().toISOString() : undefined,
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (mutation.type === 'set-request') {
|
|
253
|
+
if (isOwner(selfAid, primaryId)) {
|
|
254
|
+
throw new ContactMutationError('OWNER_PROTECTED', 'An Agent owner cannot submit a contact request');
|
|
255
|
+
}
|
|
256
|
+
contacts[primaryId] = compactEntry({
|
|
257
|
+
...existing,
|
|
258
|
+
status: 'pending',
|
|
259
|
+
pendingRequest: { ...mutation.request },
|
|
260
|
+
declinedAt: undefined,
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (mutation.type === 'review-request') {
|
|
265
|
+
if (!existing || existing.status !== 'pending' || existing.pendingRequest?.id !== mutation.requestId) {
|
|
266
|
+
throw new ContactMutationError('REQUEST_STALE', 'The contact request has changed or was already handled');
|
|
267
|
+
}
|
|
268
|
+
if (Date.parse(existing.pendingRequest.expiresAt) <= Date.parse(mutation.now)) {
|
|
269
|
+
throw new ContactMutationError('REQUEST_EXPIRED', 'The contact request has expired');
|
|
270
|
+
}
|
|
271
|
+
if (mutation.decision === 'reject') {
|
|
272
|
+
delete contacts[primaryId];
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
contacts[primaryId] = compactEntry({
|
|
276
|
+
...existing,
|
|
277
|
+
status: mutation.decision === 'block' ? 'blocked' : undefined,
|
|
278
|
+
pendingRequest: undefined,
|
|
279
|
+
declinedAt: undefined,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (mutation.type === 'expire-request') {
|
|
285
|
+
if (!existing || existing.status !== 'pending' || existing.pendingRequest?.id !== mutation.requestId)
|
|
286
|
+
return;
|
|
287
|
+
if (Date.parse(existing.pendingRequest.expiresAt) > Date.parse(mutation.now))
|
|
288
|
+
return;
|
|
289
|
+
contacts[primaryId] = compactEntry({
|
|
290
|
+
...existing,
|
|
291
|
+
status: 'declined',
|
|
292
|
+
pendingRequest: undefined,
|
|
293
|
+
declinedAt: existing.pendingRequest.expiresAt,
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (mutation.type === 'set-display-name') {
|
|
298
|
+
contacts[primaryId] = compactEntry({ ...existing, displayName: mutation.displayName ?? undefined });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (mutation.type === 'bind-alias' || mutation.type === 'bind-alias-eligible') {
|
|
302
|
+
if (mutation.type === 'bind-alias-eligible') {
|
|
303
|
+
const eligible = isOwner(selfAid, primaryId)
|
|
304
|
+
|| (!!existing && (existing.status === undefined || existing.status === 'active'));
|
|
305
|
+
if (!eligible) {
|
|
306
|
+
throw new ContactMutationError('CONTACT_NOT_ACTIVE', 'Only an active contact or Agent owner can bind an alias');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const [owner, entry] of Object.entries(contacts)) {
|
|
310
|
+
if (owner !== primaryId && entry.aliases?.includes(mutation.alias)) {
|
|
311
|
+
throw new ContactMutationError('ALIAS_CONFLICT', `Alias ${mutation.alias} is already bound to ${owner}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const aliases = Array.isArray(contacts[primaryId]?.aliases) ? [...contacts[primaryId].aliases] : [];
|
|
315
|
+
if (!aliases.includes(mutation.alias))
|
|
316
|
+
aliases.push(mutation.alias);
|
|
317
|
+
contacts[primaryId] = compactEntry({ ...contacts[primaryId], aliases });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (mutation.type === 'unbind-alias') {
|
|
321
|
+
removeAliasEverywhere(contacts, mutation.alias, primaryId);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
throw new ContactMutationError('INVALID_MUTATION', `Unsupported Contact Book mutation: ${String(mutation?.type)}`);
|
|
325
|
+
}
|
|
326
|
+
function validateContactBook(book, selfAid) {
|
|
327
|
+
const schemaErrors = validateConfig(ConfigTarget.Contact, book);
|
|
328
|
+
if (schemaErrors.length) {
|
|
329
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', schemaErrors.join('; '));
|
|
330
|
+
}
|
|
331
|
+
const aliases = new Map();
|
|
332
|
+
const configuredChannelKeys = readConfiguredContactChannelKeys(selfAid);
|
|
333
|
+
for (const [primaryId, entry] of Object.entries(book.contacts)) {
|
|
334
|
+
if (!isValidAid(primaryId) || isExplicitGroupId(primaryId)) {
|
|
335
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `Invalid contact AID: ${primaryId}`);
|
|
336
|
+
}
|
|
337
|
+
if (entry.status && isOwner(selfAid, primaryId)) {
|
|
338
|
+
throw new ContactMutationError('OWNER_PROTECTED', 'An Agent owner must remain active');
|
|
339
|
+
}
|
|
340
|
+
if (entry.status === 'pending') {
|
|
341
|
+
if (!entry.pendingRequest || !Number.isFinite(Date.parse(entry.pendingRequest.submittedAt))
|
|
342
|
+
|| !Number.isFinite(Date.parse(entry.pendingRequest.expiresAt))
|
|
343
|
+
|| Date.parse(entry.pendingRequest.expiresAt) <= Date.parse(entry.pendingRequest.submittedAt)) {
|
|
344
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `Invalid pending request for ${primaryId}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else if (entry.pendingRequest) {
|
|
348
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `pendingRequest requires pending status for ${primaryId}`);
|
|
349
|
+
}
|
|
350
|
+
if (entry.status === 'declined' && !Number.isFinite(Date.parse(entry.declinedAt ?? ''))) {
|
|
351
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `declinedAt is required for ${primaryId}`);
|
|
352
|
+
}
|
|
353
|
+
if (entry.status !== 'declined' && entry.declinedAt) {
|
|
354
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `declinedAt requires declined status for ${primaryId}`);
|
|
355
|
+
}
|
|
356
|
+
for (const alias of entry.aliases ?? []) {
|
|
357
|
+
const parsed = parseContactAlias(alias, selfAid);
|
|
358
|
+
if (!parsed) {
|
|
359
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `Invalid contact alias for ${selfAid}: ${String(alias)}`);
|
|
360
|
+
}
|
|
361
|
+
if (!configuredChannelKeys.has(parsed.channelKey)) {
|
|
362
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `Contact alias references an unconfigured channel instance: ${alias}`);
|
|
363
|
+
}
|
|
364
|
+
const owner = aliases.get(alias);
|
|
365
|
+
if (owner && owner !== primaryId) {
|
|
366
|
+
throw new ContactMutationError('ALIAS_CONFLICT', `Alias ${alias} is bound to both ${owner} and ${primaryId}`);
|
|
367
|
+
}
|
|
368
|
+
aliases.set(alias, primaryId);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function readConfiguredContactChannelKeys(selfAid) {
|
|
373
|
+
let agent;
|
|
374
|
+
try {
|
|
375
|
+
agent = atomicReadJson(agentConfig(selfAid));
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `Cannot read Agent channel inventory: ${error instanceof Error ? error.message : String(error)}`);
|
|
379
|
+
}
|
|
380
|
+
return new Set((Array.isArray(agent?.channels) ? agent.channels : [])
|
|
381
|
+
.filter((channel) => channel && typeof channel === 'object' && String(channel.type || '').toLowerCase() !== 'aun')
|
|
382
|
+
.map((channel) => {
|
|
383
|
+
const type = String(channel.type || '').trim().toLowerCase();
|
|
384
|
+
const name = String(channel.name || '').trim();
|
|
385
|
+
return type && name ? `${type}#${selfAid}#${name}` : '';
|
|
386
|
+
})
|
|
387
|
+
.filter(Boolean));
|
|
388
|
+
}
|
|
389
|
+
function cloneBook(book) {
|
|
390
|
+
const contacts = {};
|
|
391
|
+
for (const [primaryId, entry] of Object.entries(book?.contacts ?? {})) {
|
|
392
|
+
contacts[primaryId] = {
|
|
393
|
+
...entry,
|
|
394
|
+
...(entry.aliases ? { aliases: [...entry.aliases] } : {}),
|
|
395
|
+
...(entry.pendingRequest ? { pendingRequest: { ...entry.pendingRequest } } : {}),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return { $schema_version: 3, contacts };
|
|
399
|
+
}
|
|
400
|
+
function compactEntry(entry) {
|
|
401
|
+
return Object.fromEntries(Object.entries(entry).filter(([, value]) => value !== undefined));
|
|
402
|
+
}
|
|
403
|
+
function removeAliasEverywhere(contacts, alias, onlyOwner) {
|
|
404
|
+
for (const [primaryId, entry] of Object.entries(contacts)) {
|
|
405
|
+
if (onlyOwner && primaryId !== onlyOwner)
|
|
406
|
+
continue;
|
|
407
|
+
const aliases = (entry.aliases ?? []).filter(candidate => candidate !== alias);
|
|
408
|
+
if (aliases.length !== (entry.aliases ?? []).length)
|
|
409
|
+
contacts[primaryId] = compactEntry({ ...entry, aliases });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function readContactBookFromDisk(selfAid) {
|
|
413
|
+
try {
|
|
414
|
+
return atomicReadJson(agentContactConfig(selfAid));
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
throw new ContactMutationError('INVALID_CONTACT_BOOK', `Cannot read contact book: ${error instanceof Error ? error.message : String(error)}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function acquireAgentLock(selfAid) {
|
|
421
|
+
const lockDir = path.join(agentDir(selfAid), LOCK_NAME);
|
|
422
|
+
const ownerFile = path.join(lockDir, 'owner.json');
|
|
423
|
+
const lockToken = crypto.randomBytes(16).toString('hex');
|
|
424
|
+
const candidateDir = `${lockDir}.candidate-${process.pid}-${lockToken}`;
|
|
425
|
+
const candidateOwnerFile = path.join(candidateDir, 'owner.json');
|
|
426
|
+
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
|
|
427
|
+
fs.mkdirSync(candidateDir);
|
|
428
|
+
fs.writeFileSync(candidateOwnerFile, `${JSON.stringify({
|
|
429
|
+
pid: process.pid,
|
|
430
|
+
processStartedAt: Date.now() - Math.floor(process.uptime() * 1000),
|
|
431
|
+
createdAt: new Date().toISOString(),
|
|
432
|
+
lockToken,
|
|
433
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
434
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
435
|
+
let acquired = false;
|
|
436
|
+
try {
|
|
437
|
+
while (true) {
|
|
438
|
+
try {
|
|
439
|
+
fs.renameSync(candidateDir, lockDir);
|
|
440
|
+
acquired = true;
|
|
441
|
+
return () => releaseAgentLock(lockDir, ownerFile, lockToken);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
|
|
445
|
+
throw error;
|
|
446
|
+
if (removeStaleLock(lockDir, ownerFile))
|
|
447
|
+
continue;
|
|
448
|
+
if (Date.now() >= deadline) {
|
|
449
|
+
throw new ContactMutationError('LOCK_TIMEOUT', `Timed out waiting for contact mutation lock: ${lockDir}`);
|
|
450
|
+
}
|
|
451
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
if (!acquired)
|
|
457
|
+
fs.rmSync(candidateDir, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function removeStaleLock(lockDir, ownerFile) {
|
|
461
|
+
let rawOwner;
|
|
462
|
+
let pid;
|
|
463
|
+
try {
|
|
464
|
+
rawOwner = fs.readFileSync(ownerFile, 'utf8');
|
|
465
|
+
pid = Number(JSON.parse(rawOwner).pid);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
471
|
+
return false;
|
|
472
|
+
try {
|
|
473
|
+
process.kill(pid, 0);
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
if (error?.code !== 'ESRCH')
|
|
478
|
+
return false;
|
|
479
|
+
const staleClaim = `${lockDir}.reclaimed-${crypto.createHash('sha256').update(rawOwner).digest('hex').slice(0, 16)}`;
|
|
480
|
+
try {
|
|
481
|
+
fs.renameSync(lockDir, staleClaim);
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
catch (renameError) {
|
|
485
|
+
if (renameError?.code === 'ENOENT')
|
|
486
|
+
return true;
|
|
487
|
+
if (renameError?.code === 'EEXIST' || renameError?.code === 'ENOTEMPTY')
|
|
488
|
+
return false;
|
|
489
|
+
throw renameError;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function releaseAgentLock(lockDir, ownerFile, lockToken) {
|
|
494
|
+
try {
|
|
495
|
+
const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
|
|
496
|
+
if (owner.lockToken !== lockToken)
|
|
497
|
+
return;
|
|
498
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
// A missing or replaced owner file means ownership can no longer be proven.
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function recoverMutation(selfAid) {
|
|
505
|
+
const file = journalFile(selfAid);
|
|
506
|
+
if (!fs.existsSync(file))
|
|
507
|
+
return false;
|
|
508
|
+
const journal = atomicReadJson(file);
|
|
509
|
+
if (!journal || journal.schemaVersion !== 1 || journal.selfAid !== selfAid
|
|
510
|
+
|| !['prepared', 'committed'].includes(journal.state) || !Array.isArray(journal.changes)) {
|
|
511
|
+
throw new ContactMutationError('INVALID_JOURNAL', `Invalid contact mutation journal: ${file}`);
|
|
512
|
+
}
|
|
513
|
+
validateTransactionChanges(selfAid, journal.changes);
|
|
514
|
+
if (journal.state === 'prepared') {
|
|
515
|
+
for (const change of [...journal.changes].reverse()) {
|
|
516
|
+
applyTransactionChange({ ...change, after: change.before });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const change of journal.changes)
|
|
520
|
+
fileCache.invalidate(change.file);
|
|
521
|
+
clearRoleStoreCache(selfAid);
|
|
522
|
+
removeJournal(selfAid);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
function validateTransactionChanges(selfAid, changes) {
|
|
526
|
+
const root = path.resolve(agentDir(selfAid));
|
|
527
|
+
const seen = new Set();
|
|
528
|
+
for (const change of changes) {
|
|
529
|
+
if (!change || typeof change !== 'object' || typeof change.file !== 'string' || !change.file) {
|
|
530
|
+
throw new ContactMutationError('INVALID_TRANSACTION', 'Transaction changes must contain a file path');
|
|
531
|
+
}
|
|
532
|
+
const file = path.resolve(change.file);
|
|
533
|
+
const relative = path.relative(root, file);
|
|
534
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
535
|
+
throw new ContactMutationError('UNSAFE_TRANSACTION', `Transaction file is outside the Agent directory: ${file}`);
|
|
536
|
+
}
|
|
537
|
+
if (seen.has(file)) {
|
|
538
|
+
throw new ContactMutationError('INVALID_TRANSACTION', `Transaction contains duplicate file changes: ${file}`);
|
|
539
|
+
}
|
|
540
|
+
seen.add(file);
|
|
541
|
+
if ((change.before !== null && typeof change.before !== 'string')
|
|
542
|
+
|| (change.after !== null && typeof change.after !== 'string')) {
|
|
543
|
+
throw new ContactMutationError('INVALID_TRANSACTION', `Invalid transaction content for ${file}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function applyTransactionChange(change) {
|
|
548
|
+
if (change.after === null) {
|
|
549
|
+
for (const candidate of [change.file, `${change.file}_`, `${change.file}__`]) {
|
|
550
|
+
fs.rmSync(candidate, { force: true });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
atomicWrite(change.file, change.after);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function writeJournal(selfAid, journal) {
|
|
558
|
+
atomicWriteJson(journalFile(selfAid), journal);
|
|
559
|
+
}
|
|
560
|
+
function removeJournal(selfAid) {
|
|
561
|
+
const file = journalFile(selfAid);
|
|
562
|
+
for (const candidate of [file, `${file}_`, `${file}__`])
|
|
563
|
+
fs.rmSync(candidate, { force: true });
|
|
564
|
+
}
|
|
565
|
+
function journalFile(selfAid) {
|
|
566
|
+
return path.join(agentDir(selfAid), JOURNAL_NAME);
|
|
567
|
+
}
|
|
568
|
+
function appendAudit(selfAid, request, previousContactRevision, contactRevisionValue) {
|
|
569
|
+
const file = path.join(resolvePaths().dataDir, 'contact-book-audit.jsonl');
|
|
570
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
571
|
+
fs.appendFileSync(file, `${JSON.stringify({
|
|
572
|
+
timestamp: new Date().toISOString(),
|
|
573
|
+
selfAid,
|
|
574
|
+
actor: request.actor ?? 'system',
|
|
575
|
+
mutation: request.mutation,
|
|
576
|
+
previousContactRevision,
|
|
577
|
+
contactRevision: contactRevisionValue,
|
|
578
|
+
})}\n`, { mode: 0o600 });
|
|
579
|
+
}
|
|
580
|
+
function isOwner(selfAid, primaryId) {
|
|
581
|
+
try {
|
|
582
|
+
const config = atomicReadJson(path.join(agentDir(selfAid), 'config.json'));
|
|
583
|
+
return config?.owners?.some(value => String(value || '').trim() === primaryId) ?? false;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function normalizeAid(value, label) {
|
|
590
|
+
const aid = String(value || '').trim();
|
|
591
|
+
if (!isValidAid(aid))
|
|
592
|
+
throw new ContactMutationError('INVALID_AID', `Invalid ${label}: ${JSON.stringify(value)}`);
|
|
593
|
+
return aid;
|
|
594
|
+
}
|
|
595
|
+
function normalizeContactAid(value) {
|
|
596
|
+
const aid = normalizeAid(value, 'contact AID');
|
|
597
|
+
if (isExplicitGroupId(aid)) {
|
|
598
|
+
throw new ContactMutationError('INVALID_AID', `Contact Book does not support group AID: ${JSON.stringify(value)}`);
|
|
599
|
+
}
|
|
600
|
+
return aid;
|
|
601
|
+
}
|
|
602
|
+
function stableStringify(value) {
|
|
603
|
+
if (Array.isArray(value))
|
|
604
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
605
|
+
if (value && typeof value === 'object') {
|
|
606
|
+
return `{${Object.entries(value)
|
|
607
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
608
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
|
|
609
|
+
.join(',')}}`;
|
|
610
|
+
}
|
|
611
|
+
return JSON.stringify(value);
|
|
612
|
+
}
|
|
613
|
+
function readTextOrNull(file) {
|
|
614
|
+
try {
|
|
615
|
+
return fs.readFileSync(file, 'utf8');
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
if (error?.code === 'ENOENT')
|
|
619
|
+
return null;
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
3
|
+
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
4
|
+
import { assertContactBookV2Ready, ConfigError } from './config-manager.js';
|
|
5
|
+
import { recoverAllContactBookMutationsSync } from './contact-book-store.js';
|
|
6
|
+
export async function ensureContactBookV2OnStartup() {
|
|
7
|
+
try {
|
|
8
|
+
recoverAllContactBookMutationsSync();
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
12
|
+
throw new ConfigError('CONTACT_BOOK_V2_RECOVERY_FAILED', `Interrupted contact transaction recovery failed: ${detail}`, { cause: error });
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
assertContactBookV2Ready();
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!(error instanceof ConfigError) || error.code !== 'CONTACT_BOOK_V2_MIGRATION_REQUIRED')
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const home = resolvePaths().root;
|
|
23
|
+
const script = path.join(getPackageRoot(), 'kits', 'migrations', 'migrate-contact-book-v2.mjs');
|
|
24
|
+
try {
|
|
25
|
+
const migration = await import(pathToFileURL(script).href);
|
|
26
|
+
const result = migration.runMigration({ home, apply: true });
|
|
27
|
+
assertContactBookV2Ready();
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new ConfigError('CONTACT_BOOK_V2_MIGRATION_FAILED', `Automatic contact book v2 migration failed: ${detail}\n` +
|
|
33
|
+
`node kits/migrations/migrate-contact-book-v2.mjs --apply --home ${JSON.stringify(home)}`, { cause: error });
|
|
34
|
+
}
|
|
35
|
+
}
|