evolcore 0.0.19 → 0.0.21

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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -0,0 +1,550 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { resolvePaths } from '../paths.js';
5
+ import { ConfigError } from './config-manager.js';
6
+ const SUPPORTED_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'ecagent']);
7
+ const BASEAGENT_ALIASES = new Map([
8
+ ['cc', 'claude'],
9
+ ['claude-code', 'claude'],
10
+ ['claude code', 'claude'],
11
+ ['claudecode', 'claude'],
12
+ ['codex-cli', 'codex'],
13
+ ['codex cli', 'codex'],
14
+ ['gemini-cli', 'gemini'],
15
+ ['gemini cli', 'gemini'],
16
+ ['geminicli', 'gemini'],
17
+ ['ec-agent', 'ecagent'],
18
+ ]);
19
+ const AUXILIARY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
20
+ function isRecord(value) {
21
+ return !!value && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+ function hasOwn(value, field) {
24
+ return Object.prototype.hasOwnProperty.call(value, field);
25
+ }
26
+ function readJson(file) {
27
+ const text = fs.readFileSync(file, 'utf8');
28
+ return { value: JSON.parse(text), text };
29
+ }
30
+ function jsonText(value) {
31
+ return `${JSON.stringify(value, null, 2)}\n`;
32
+ }
33
+ function canonicalBaseagent(value) {
34
+ const key = String(value || '').trim().toLowerCase().replace(/_/g, '-');
35
+ const canonical = BASEAGENT_ALIASES.get(key) || key;
36
+ return SUPPORTED_BASEAGENTS.has(canonical) ? canonical : undefined;
37
+ }
38
+ function validAid(name) {
39
+ const labels = name.split('.');
40
+ return labels.length >= 3
41
+ && labels.every(label => /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label));
42
+ }
43
+ function baseagentSource(value) {
44
+ return isRecord(value.baseagents)
45
+ ? value.baseagents
46
+ : isRecord(value.agents)
47
+ ? value.agents
48
+ : undefined;
49
+ }
50
+ function baseagentNames(value) {
51
+ const source = baseagentSource(value);
52
+ if (!source)
53
+ return [];
54
+ const names = [];
55
+ for (const rawName of Object.keys(source)) {
56
+ const name = canonicalBaseagent(rawName);
57
+ if (name && !names.includes(name))
58
+ names.push(name);
59
+ }
60
+ return names;
61
+ }
62
+ function inheritedContext(value, parent) {
63
+ const activeBaseagent = canonicalBaseagent(value.active_baseagent) || parent?.activeBaseagent;
64
+ const names = [...new Set([...baseagentNames(value), ...(parent?.baseagents || [])])];
65
+ const auxiliaryModels = { ...(parent?.auxiliaryModels || {}) };
66
+ const source = baseagentSource(value);
67
+ if (source) {
68
+ for (const [rawName, rawBlock] of Object.entries(source)) {
69
+ const name = canonicalBaseagent(rawName);
70
+ if (name && isRecord(rawBlock) && typeof rawBlock.auxiliaryModel === 'string' && rawBlock.auxiliaryModel.trim()) {
71
+ auxiliaryModels[name] = rawBlock.auxiliaryModel.trim();
72
+ }
73
+ }
74
+ }
75
+ return { activeBaseagent, baseagents: names, auxiliaryModels };
76
+ }
77
+ function inferStringTargets(value, parent, file, source) {
78
+ const active = canonicalBaseagent(value.active_baseagent) || parent.activeBaseagent;
79
+ if (active)
80
+ return [active];
81
+ const local = baseagentNames(value);
82
+ if (local.length === 1)
83
+ return local;
84
+ if (parent.baseagents.length === 1)
85
+ return parent.baseagents;
86
+ const inherited = Object.keys(parent.auxiliaryModels);
87
+ if (inherited.length === 1)
88
+ return inherited;
89
+ throw new Error(`${file}: cannot determine baseagent for string ${source}; `
90
+ + 'set active_baseagent or create one baseagents.<name> block before migration');
91
+ }
92
+ function legacyBuckets(value) {
93
+ if (!isRecord(value.responseModeParams))
94
+ return [];
95
+ const params = value.responseModeParams;
96
+ const keys = Object.keys(params);
97
+ const preferred = typeof value.responseMode === 'string' ? value.responseMode.trim() : '';
98
+ const ordered = [];
99
+ for (const key of [preferred, 'single-session', 'dual-session', ...keys.sort()]) {
100
+ if (key && keys.includes(key) && !ordered.includes(key))
101
+ ordered.push(key);
102
+ }
103
+ const buckets = [];
104
+ for (const modeId of ordered) {
105
+ const bucket = params[modeId];
106
+ if (isRecord(bucket) && (hasOwn(bucket, 'auxiliaryModel') || hasOwn(bucket, 'auxiliaryEffort'))) {
107
+ buckets.push([modeId, bucket]);
108
+ }
109
+ }
110
+ return buckets;
111
+ }
112
+ function parseLegacyModel(raw, file, source) {
113
+ if (typeof raw === 'string') {
114
+ if (!raw.trim())
115
+ throw new Error(`${file}: ${source} must not be empty`);
116
+ return { kind: 'string', value: raw.trim() };
117
+ }
118
+ if (!isRecord(raw) || Object.keys(raw).length === 0) {
119
+ throw new Error(`${file}: ${source} must be a non-empty model string or baseagent map`);
120
+ }
121
+ const values = [];
122
+ for (const [rawName, model] of Object.entries(raw)) {
123
+ const name = canonicalBaseagent(rawName);
124
+ if (!name)
125
+ throw new Error(`${file}: ${source}.${rawName} is not a supported baseagent`);
126
+ if (typeof model !== 'string' || !model.trim()) {
127
+ throw new Error(`${file}: ${source}.${rawName} must be a non-empty string`);
128
+ }
129
+ values.push([name, model.trim()]);
130
+ }
131
+ return { kind: 'map', values };
132
+ }
133
+ function setCandidate(candidates, target, value, source, file) {
134
+ const previous = candidates.get(target);
135
+ if (previous && previous.value !== value) {
136
+ throw new Error(`${file}: conflicting legacy auxiliary values for baseagents.${target} `
137
+ + `(${previous.source}=${JSON.stringify(previous.value)}, ${source}=${JSON.stringify(value)})`);
138
+ }
139
+ if (!previous)
140
+ candidates.set(target, { value, source });
141
+ }
142
+ function parseEffort(raw, file, source) {
143
+ if (typeof raw !== 'string' || !AUXILIARY_EFFORTS.has(raw)) {
144
+ throw new Error(`${file}: ${source} must be one of low, medium, high, xhigh, max`);
145
+ }
146
+ return raw;
147
+ }
148
+ function cloneBaseagents(source) {
149
+ if (!source)
150
+ return {};
151
+ return Object.fromEntries(Object.entries(source).map(([name, block]) => [name, isRecord(block) ? { ...block } : block]));
152
+ }
153
+ function migrateConfig(input, file, parentContext) {
154
+ if (!isRecord(input))
155
+ throw new Error(`${file}: config must be an object`);
156
+ const buckets = legacyBuckets(input);
157
+ if (buckets.length === 0)
158
+ return { value: input, changed: false, summary: {} };
159
+ const context = inheritedContext(input, parentContext);
160
+ const models = new Map();
161
+ const efforts = [];
162
+ for (const [modeId, bucket] of buckets) {
163
+ const sourcePrefix = `responseModeParams.${modeId}`;
164
+ if (hasOwn(bucket, 'auxiliaryModel')) {
165
+ const source = `${sourcePrefix}.auxiliaryModel`;
166
+ const parsed = parseLegacyModel(bucket.auxiliaryModel, file, source);
167
+ const entries = parsed.kind === 'map'
168
+ ? parsed.values
169
+ : inferStringTargets(input, context, file, source).map(name => [name, parsed.value]);
170
+ for (const [target, model] of entries)
171
+ setCandidate(models, target, model, source, file);
172
+ }
173
+ if (hasOwn(bucket, 'auxiliaryEffort')) {
174
+ const source = `${sourcePrefix}.auxiliaryEffort`;
175
+ efforts.push({ value: parseEffort(bucket.auxiliaryEffort, file, source), source });
176
+ }
177
+ }
178
+ const existingBaseagents = baseagentSource(input);
179
+ const existingModelTargets = [];
180
+ if (existingBaseagents) {
181
+ for (const [rawName, rawBlock] of Object.entries(existingBaseagents)) {
182
+ const name = canonicalBaseagent(rawName);
183
+ if (name && isRecord(rawBlock) && hasOwn(rawBlock, 'auxiliaryModel'))
184
+ existingModelTargets.push(name);
185
+ }
186
+ }
187
+ const effortTargets = models.size > 0
188
+ ? [...models.keys()]
189
+ : existingModelTargets.length > 0
190
+ ? [...new Set(existingModelTargets)]
191
+ : inferStringTargets(input, context, file, 'legacy auxiliaryEffort');
192
+ const nextBaseagents = cloneBaseagents(existingBaseagents);
193
+ let movedModels = 0;
194
+ let movedEfforts = 0;
195
+ for (const [target, candidate] of models) {
196
+ const current = nextBaseagents[target];
197
+ if (current !== undefined && !isRecord(current)) {
198
+ throw new Error(`${file}: baseagents.${target} must be an object`);
199
+ }
200
+ const block = isRecord(current) ? current : {};
201
+ if (hasOwn(block, 'auxiliaryModel') && block.auxiliaryModel !== candidate.value) {
202
+ throw new Error(`${file}: baseagents.${target}.auxiliaryModel conflicts with ${candidate.source}; `
203
+ + 'existing value is preserved only after manual resolution');
204
+ }
205
+ if (!hasOwn(block, 'auxiliaryModel')) {
206
+ block.auxiliaryModel = candidate.value;
207
+ movedModels += 1;
208
+ }
209
+ nextBaseagents[target] = block;
210
+ }
211
+ const effortsByBaseagent = new Map();
212
+ for (const effort of efforts) {
213
+ for (const target of effortTargets)
214
+ setCandidate(effortsByBaseagent, target, effort.value, effort.source, file);
215
+ }
216
+ for (const [target, candidate] of effortsByBaseagent) {
217
+ let block = nextBaseagents[target];
218
+ if (block === undefined && context.auxiliaryModels[target]) {
219
+ block = { auxiliaryModel: context.auxiliaryModels[target] };
220
+ nextBaseagents[target] = block;
221
+ }
222
+ if (!isRecord(block)) {
223
+ throw new Error(`${file}: ${candidate.source} has no migrated baseagents.${target}.auxiliaryModel`);
224
+ }
225
+ if (typeof block.auxiliaryModel !== 'string' || !block.auxiliaryModel.trim()) {
226
+ const inheritedModel = context.auxiliaryModels[target];
227
+ if (inheritedModel)
228
+ block.auxiliaryModel = inheritedModel;
229
+ }
230
+ if (typeof block.auxiliaryModel !== 'string' || !block.auxiliaryModel.trim()) {
231
+ throw new Error(`${file}: ${candidate.source} has no migrated baseagents.${target}.auxiliaryModel`);
232
+ }
233
+ if (hasOwn(block, 'auxiliaryEffort') && block.auxiliaryEffort !== candidate.value) {
234
+ throw new Error(`${file}: baseagents.${target}.auxiliaryEffort conflicts with ${candidate.source}; `
235
+ + 'existing value is preserved only after manual resolution');
236
+ }
237
+ if (!hasOwn(block, 'auxiliaryEffort')) {
238
+ block.auxiliaryEffort = candidate.value;
239
+ movedEfforts += 1;
240
+ }
241
+ }
242
+ const nextParams = { ...input.responseModeParams };
243
+ for (const [modeId, bucket] of buckets) {
244
+ const nextBucket = { ...bucket };
245
+ delete nextBucket.auxiliaryModel;
246
+ delete nextBucket.auxiliaryEffort;
247
+ nextParams[modeId] = nextBucket;
248
+ }
249
+ const next = {
250
+ ...input,
251
+ ...(Object.keys(nextBaseagents).length > 0 ? { baseagents: nextBaseagents } : {}),
252
+ responseModeParams: nextParams,
253
+ };
254
+ if (hasOwn(input, 'agents'))
255
+ delete next.agents;
256
+ return {
257
+ value: next,
258
+ changed: true,
259
+ summary: { movedModels, movedEfforts, cleanedBuckets: buckets.length },
260
+ };
261
+ }
262
+ function walkRelationConfigs(dir) {
263
+ if (!fs.existsSync(dir))
264
+ return [];
265
+ const files = [];
266
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
267
+ .sort((left, right) => left.name.localeCompare(right.name));
268
+ for (const entry of entries) {
269
+ const full = path.join(dir, entry.name);
270
+ if (entry.isDirectory())
271
+ files.push(...walkRelationConfigs(full));
272
+ else if (entry.isFile() && entry.name === 'config.json')
273
+ files.push(full);
274
+ }
275
+ return files;
276
+ }
277
+ function planConfigFile(file, parentContext, changes, summary) {
278
+ const { value, text } = readJson(file);
279
+ const result = migrateConfig(value, file, parentContext);
280
+ if (!result.changed)
281
+ return result.value;
282
+ const after = jsonText(result.value);
283
+ if (after !== text)
284
+ changes.push({ file, kind: 'auxiliary-model', before: text, after });
285
+ summary.filesChanged += 1;
286
+ summary.movedModels += result.summary.movedModels || 0;
287
+ summary.movedEfforts += result.summary.movedEfforts || 0;
288
+ summary.cleanedBuckets += result.summary.cleanedBuckets || 0;
289
+ return result.value;
290
+ }
291
+ function planAuxiliaryModelMigration(home) {
292
+ const changes = [];
293
+ const summary = {
294
+ configsScanned: 0,
295
+ filesChanged: 0,
296
+ movedModels: 0,
297
+ movedEfforts: 0,
298
+ cleanedBuckets: 0,
299
+ };
300
+ const agentsRoot = path.join(home, 'agents');
301
+ if (!fs.existsSync(agentsRoot))
302
+ return { home, changes, summary };
303
+ const defaultsFile = path.join(agentsRoot, 'defaults.json');
304
+ let defaults = {};
305
+ if (fs.existsSync(defaultsFile)) {
306
+ summary.configsScanned += 1;
307
+ defaults = planConfigFile(defaultsFile, undefined, changes, summary);
308
+ }
309
+ const defaultsContext = inheritedContext(defaults);
310
+ const agentEntries = fs.readdirSync(agentsRoot, { withFileTypes: true })
311
+ .sort((left, right) => left.name.localeCompare(right.name));
312
+ for (const entry of agentEntries) {
313
+ if (!entry.isDirectory() || !validAid(entry.name))
314
+ continue;
315
+ const agentDir = path.join(agentsRoot, entry.name);
316
+ const agentFile = path.join(agentDir, 'config.json');
317
+ if (!fs.existsSync(agentFile))
318
+ continue;
319
+ summary.configsScanned += 1;
320
+ const agent = planConfigFile(agentFile, defaultsContext, changes, summary);
321
+ const agentContext = inheritedContext(agent, defaultsContext);
322
+ for (const relationFile of walkRelationConfigs(path.join(agentDir, 'relations'))) {
323
+ summary.configsScanned += 1;
324
+ planConfigFile(relationFile, agentContext, changes, summary);
325
+ }
326
+ }
327
+ return { home, changes, summary };
328
+ }
329
+ function processIsAlive(pid) {
330
+ try {
331
+ process.kill(pid, 0);
332
+ return true;
333
+ }
334
+ catch (error) {
335
+ if (isRecord(error) && error.code === 'ESRCH')
336
+ return false;
337
+ if (isRecord(error) && error.code === 'EPERM')
338
+ return true;
339
+ throw error;
340
+ }
341
+ }
342
+ function assertDaemonStopped(home) {
343
+ const candidates = [];
344
+ const pidFile = path.join(home, 'daemon.pid');
345
+ if (fs.existsSync(pidFile)) {
346
+ candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
347
+ }
348
+ const instanceDir = path.join(home, 'data', 'instance');
349
+ if (fs.existsSync(instanceDir)) {
350
+ for (const name of fs.readdirSync(instanceDir)) {
351
+ if (!/^main-\d+\.json$/.test(name))
352
+ continue;
353
+ const file = path.join(instanceDir, name);
354
+ try {
355
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
356
+ if (isRecord(record))
357
+ candidates.push({ pid: Number(record.pid), source: file });
358
+ }
359
+ catch {
360
+ // Invalid instance records are handled by the instance registry.
361
+ }
362
+ }
363
+ }
364
+ for (const { pid, source } of candidates) {
365
+ if (Number.isInteger(pid) && pid > 0 && processIsAlive(pid)) {
366
+ throw new Error(`EvolCore daemon appears to be running (pid=${pid}, record=${source}); stop it before migration`);
367
+ }
368
+ }
369
+ }
370
+ function lockPath(home) {
371
+ return path.join(home, 'data', 'auxiliary-model-v1-migration.lock');
372
+ }
373
+ function acquireLock(home) {
374
+ const dir = lockPath(home);
375
+ const ownerFile = path.join(dir, 'owner.json');
376
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
377
+ try {
378
+ fs.mkdirSync(dir);
379
+ }
380
+ catch (error) {
381
+ if (!isRecord(error) || error.code !== 'EEXIST')
382
+ throw error;
383
+ let owner;
384
+ try {
385
+ owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
386
+ }
387
+ catch {
388
+ throw new Error(`Migration lock is unreadable: ${dir}`);
389
+ }
390
+ const pid = isRecord(owner) ? Number(owner.pid) : NaN;
391
+ if (Number.isInteger(pid) && pid > 0 && processIsAlive(pid)) {
392
+ throw new Error(`Another auxiliary model migration is already running (pid=${pid})`);
393
+ }
394
+ fs.rmSync(dir, { recursive: true, force: true });
395
+ fs.mkdirSync(dir);
396
+ }
397
+ fs.writeFileSync(ownerFile, jsonText({ pid: process.pid, createdAt: new Date().toISOString() }));
398
+ return () => fs.rmSync(dir, { recursive: true, force: true });
399
+ }
400
+ function atomicWrite(file, content) {
401
+ fs.mkdirSync(path.dirname(file), { recursive: true });
402
+ const mode = fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600;
403
+ const temp = `${file}.auxiliary-model-${process.pid}-${crypto.randomBytes(6).toString('hex')}.tmp`;
404
+ fs.writeFileSync(temp, content, { mode });
405
+ fs.renameSync(temp, file);
406
+ }
407
+ function journalPath(home) {
408
+ return path.join(home, 'data', 'auxiliary-model-v1-migration.json');
409
+ }
410
+ function writeJournal(plan, state) {
411
+ const journal = {
412
+ schemaVersion: 1,
413
+ state,
414
+ home: plan.home,
415
+ changes: plan.changes,
416
+ };
417
+ atomicWrite(journalPath(plan.home), jsonText(journal));
418
+ }
419
+ function readJournal(home) {
420
+ const file = journalPath(home);
421
+ if (!fs.existsSync(file))
422
+ return undefined;
423
+ let raw;
424
+ try {
425
+ raw = JSON.parse(fs.readFileSync(file, 'utf8'));
426
+ }
427
+ catch {
428
+ throw new Error(`Migration journal is unreadable: ${file}`);
429
+ }
430
+ if (!isRecord(raw) || raw.schemaVersion !== 1
431
+ || (raw.state !== 'prepared' && raw.state !== 'committed')
432
+ || typeof raw.home !== 'string' || path.resolve(raw.home) !== path.resolve(home)
433
+ || !Array.isArray(raw.changes)) {
434
+ throw new Error(`Migration journal is invalid: ${file}`);
435
+ }
436
+ const rootPrefix = `${path.resolve(home)}${path.sep}`;
437
+ const changes = [];
438
+ for (const item of raw.changes) {
439
+ if (!isRecord(item) || typeof item.file !== 'string' || typeof item.after !== 'string'
440
+ || (item.before !== null && typeof item.before !== 'string')) {
441
+ throw new Error(`Migration journal contains an unsafe change: ${file}`);
442
+ }
443
+ const target = path.resolve(item.file);
444
+ if (!target.startsWith(rootPrefix)) {
445
+ throw new Error(`Migration journal contains an unsafe change: ${file}`);
446
+ }
447
+ changes.push({
448
+ file: item.file,
449
+ kind: 'auxiliary-model',
450
+ before: item.before,
451
+ after: item.after,
452
+ });
453
+ }
454
+ return {
455
+ schemaVersion: 1,
456
+ state: raw.state,
457
+ home: raw.home,
458
+ changes,
459
+ };
460
+ }
461
+ function recoverJournal(home) {
462
+ const journal = readJournal(home);
463
+ if (!journal)
464
+ return;
465
+ if (journal.state === 'prepared') {
466
+ for (const item of [...journal.changes].reverse()) {
467
+ if (item.before === null)
468
+ fs.rmSync(item.file, { force: true });
469
+ else
470
+ atomicWrite(item.file, item.before);
471
+ }
472
+ }
473
+ fs.rmSync(journalPath(home), { force: true });
474
+ }
475
+ function applyMigration(plan) {
476
+ if (plan.changes.length === 0)
477
+ return null;
478
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
479
+ const backup = path.join(plan.home, 'backups', 'auxiliary-model-v1-migration', stamp);
480
+ fs.mkdirSync(path.join(backup, 'files'), { recursive: true });
481
+ for (const item of plan.changes) {
482
+ if (item.before === null)
483
+ continue;
484
+ const destination = path.join(backup, 'files', path.relative(plan.home, item.file));
485
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
486
+ fs.writeFileSync(destination, item.before);
487
+ }
488
+ fs.writeFileSync(path.join(backup, 'manifest.json'), jsonText({
489
+ schemaVersion: 1,
490
+ createdAt: new Date().toISOString(),
491
+ summary: plan.summary,
492
+ files: plan.changes.map(item => ({
493
+ path: path.relative(plan.home, item.file),
494
+ kind: item.kind,
495
+ })),
496
+ }));
497
+ writeJournal(plan, 'prepared');
498
+ try {
499
+ for (const item of plan.changes)
500
+ atomicWrite(item.file, item.after);
501
+ writeJournal(plan, 'committed');
502
+ fs.rmSync(journalPath(plan.home), { force: true });
503
+ }
504
+ catch (error) {
505
+ recoverJournal(plan.home);
506
+ throw error;
507
+ }
508
+ return backup;
509
+ }
510
+ /** Plan or apply the legacy response-mode bucket to baseagent schema migration. */
511
+ export function runAuxiliaryModelSchemaMigration(options = {}) {
512
+ const home = path.resolve(options.home || resolvePaths().root);
513
+ const release = acquireLock(home);
514
+ try {
515
+ assertDaemonStopped(home);
516
+ recoverJournal(home);
517
+ const plan = planAuxiliaryModelMigration(home);
518
+ const backup = options.apply ? applyMigration(plan) : null;
519
+ return {
520
+ ok: true,
521
+ mode: options.apply ? 'applied' : 'dry-run',
522
+ home,
523
+ changedFiles: plan.changes.length,
524
+ backup,
525
+ summary: plan.summary,
526
+ };
527
+ }
528
+ finally {
529
+ release();
530
+ }
531
+ }
532
+ /** Apply pending configuration schema migrations before daemon registration. */
533
+ export async function ensureAuxiliaryModelMigrationOnStartup() {
534
+ const home = resolvePaths().root;
535
+ try {
536
+ const result = runAuxiliaryModelSchemaMigration({ home, apply: true });
537
+ if (result.changedFiles === 0)
538
+ return null;
539
+ return {
540
+ mode: 'applied',
541
+ changedFiles: result.changedFiles,
542
+ backup: result.backup,
543
+ summary: result.summary,
544
+ };
545
+ }
546
+ catch (error) {
547
+ const detail = error instanceof Error ? error.message : String(error);
548
+ throw new ConfigError('AUXILIARY_MODEL_MIGRATION_FAILED', `Automatic Session Renew auxiliary-model schema migration failed: ${detail}`, { cause: error });
549
+ }
550
+ }