ec-web 0.0.21 → 0.0.23

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.
@@ -1,117 +1,44 @@
1
1
  /**
2
2
  * Base agent 环境检测 — 检测本机可用的 Claude、Codex、Gemini 和内置 EC Agent。
3
3
  *
4
- * 检测条件:
5
- * 命令行工具优先按 PATH 检测;同时保留会话目录检测,兼容 CLI 不在当前 shell
6
- * PATH 但已经在本机初始化过的情况。ecagent 随 EvolCore 一起提供,始终作为可选
7
- * 基座展示,实际凭据和网关配置由配置校验负责。
4
+ * 检测条件与 ec init 共用:Claude/Gemini 按可执行文件检测,Codex 要求满足
5
+ * 版本及 app-server 条件,ecagent 随 EvolCore 内置。不会仅因历史会话目录存在
6
+ * 就判定 CLI 可用。
8
7
  */
9
- import fs from 'fs';
10
- import path from 'path';
11
- import os from 'os';
12
- import { createRequire } from 'module';
13
- import { spawnSync } from 'child_process';
14
- const requireFromHere = createRequire(import.meta.url);
15
- let _cached = null;
16
- /**
17
- * 检测 node:sqlite 模块是否可用(Node 22.5+)
18
- */
19
- function checkSqliteAvailable() {
20
- try {
21
- requireFromHere('node:sqlite');
22
- return true;
23
- }
24
- catch {
25
- return false;
8
+ import { resolveParentDistModule, toFileUrl } from './parent-package.js';
9
+ let sharedDetectorPromise = null;
10
+ async function sharedDetector() {
11
+ if (!sharedDetectorPromise) {
12
+ sharedDetectorPromise = import(toFileUrl(resolveParentDistModule('agents', 'baseagent-availability.js')));
26
13
  }
27
- }
28
- /**
29
- * 检测 Codex state_*.sqlite 文件是否存在
30
- */
31
- function checkCodexStateDb() {
32
- const codexHome = path.join(os.homedir(), '.codex');
33
- if (!fs.existsSync(codexHome))
34
- return false;
35
- try {
36
- const files = fs.readdirSync(codexHome).filter(f => /^state_\d+\.sqlite$/.test(f));
37
- return files.length > 0;
38
- }
39
- catch {
40
- return false;
41
- }
42
- }
43
- /**
44
- * 检测 Claude projects 目录是否存在
45
- */
46
- function checkClaudeProjects() {
47
- const claudeProjects = path.join(os.homedir(), '.claude', 'projects');
48
- return fs.existsSync(claudeProjects);
49
- }
50
- function commandExists(command) {
51
- try {
52
- const result = process.platform === 'win32'
53
- ? spawnSync('where', [command], { stdio: 'ignore', windowsHide: true })
54
- : spawnSync('sh', ['-lc', `command -v ${JSON.stringify(command)}`], { stdio: 'ignore' });
55
- return result.status === 0;
56
- }
57
- catch {
58
- return false;
59
- }
60
- }
61
- function checkGeminiHome() {
62
- return fs.existsSync(path.join(os.homedir(), '.gemini'));
63
- }
64
- function detectSources(id) {
65
- if (id === 'ecagent')
66
- return ['内置'];
67
- const sources = [];
68
- if (commandExists(id))
69
- sources.push('PATH');
70
- if (id === 'claude' && checkClaudeProjects())
71
- sources.push('~/.claude/projects');
72
- if (id === 'codex') {
73
- if (checkCodexSessions())
74
- sources.push('~/.codex/sessions');
75
- if (checkCodexStateDb())
76
- sources.push('~/.codex/state_*.sqlite');
77
- if (checkSqliteAvailable())
78
- sources.push('node:sqlite');
79
- }
80
- if (id === 'gemini' && checkGeminiHome())
81
- sources.push('~/.gemini');
82
- return sources;
83
- }
84
- /**
85
- * 检测 Codex sessions 目录是否存在
86
- */
87
- function checkCodexSessions() {
88
- const codexSessions = path.join(os.homedir(), '.codex', 'sessions');
89
- return fs.existsSync(codexSessions);
14
+ return sharedDetectorPromise;
90
15
  }
91
16
  /**
92
17
  * 检测所有 base agent 的可用性
93
- * 结果会缓存,避免重复检测
18
+ * 每次调用都会重新探测,确保基础设置中的“重新检测”反映当前环境。
94
19
  */
95
- export function detectBaseAgents() {
96
- if (_cached)
97
- return _cached;
98
- const claude = detectSources('claude').length > 0;
99
- const codex = detectSources('codex').length > 0;
100
- const gemini = detectSources('gemini').length > 0;
101
- const ecagent = true;
102
- _cached = { claude, codex, gemini, ecagent };
103
- return _cached;
20
+ export async function detectBaseAgentsAsync() {
21
+ const mod = await sharedDetector();
22
+ const detected = new Set(mod.detectAvailableBaseagents());
23
+ return {
24
+ claude: detected.has('claude'),
25
+ codex: detected.has('codex'),
26
+ gemini: detected.has('gemini'),
27
+ ecagent: detected.has('ecagent'),
28
+ };
104
29
  }
105
30
  /** Return display-oriented detection details for the Basic Settings API. */
106
- export function detectBaseAgentDetails(defaults = {}) {
31
+ export async function detectBaseAgentDetails(defaults = {}) {
32
+ const mod = await sharedDetector();
33
+ const probes = mod.detectBaseagentDetails();
107
34
  const configured = isRecord(defaults?.baseagents) ? defaults.baseagents : {};
108
- return ['claude', 'codex', 'gemini', 'ecagent'].map(id => {
109
- const detectionSource = detectSources(id);
35
+ return probes.map((probe) => {
36
+ const id = probe.id;
110
37
  return {
111
38
  id,
112
- detected: id === 'ecagent' || detectionSource.length > 0,
39
+ detected: probe.detected,
113
40
  configured: Object.prototype.hasOwnProperty.call(configured, id),
114
- detectionSource,
41
+ detectionSource: probe.detectionSource,
115
42
  };
116
43
  });
117
44
  }
@@ -121,6 +48,4 @@ function isRecord(value) {
121
48
  /**
122
49
  * 重置缓存(测试用)
123
50
  */
124
- export function resetDetectionCache() {
125
- _cached = null;
126
- }
51
+ export function resetDetectionCache() { }
@@ -176,6 +176,10 @@ function baseagentPath(fieldPath) {
176
176
  return null;
177
177
  return { baseagent: match[1], field: match[2] };
178
178
  }
179
+ function baseagentBlockPath(fieldPath) {
180
+ const match = fieldPath.match(/^baseagents\.([a-z0-9_-]+)$/);
181
+ return match?.[1] || null;
182
+ }
179
183
  export function isAllowedBasicSettingsPath(file, fieldPath) {
180
184
  try {
181
185
  pathSegments(fieldPath);
@@ -187,6 +191,9 @@ export function isAllowedBasicSettingsPath(file, fieldPath) {
187
191
  return DAEMON_EXACT_PATHS.has(fieldPath);
188
192
  if (DEFAULTS_EXACT_PATHS.has(fieldPath))
189
193
  return true;
194
+ const block = baseagentBlockPath(fieldPath);
195
+ if (block)
196
+ return BASEAGENT_SET.has(block);
190
197
  const parsed = baseagentPath(fieldPath);
191
198
  return !!parsed && BASEAGENT_SET.has(parsed.baseagent) && BASEAGENT_FIELDS[parsed.baseagent]?.has(parsed.field);
192
199
  }
@@ -450,6 +457,11 @@ function validateDefaultsValue(fieldPath, value) {
450
457
  const parsed = baseagentPath(fieldPath);
451
458
  if (parsed)
452
459
  return validateBaseagentValue(parsed.baseagent, parsed.field, fieldPath, value);
460
+ if (baseagentBlockPath(fieldPath)) {
461
+ if (!isRecord(value) || Object.keys(value).length > 0)
462
+ throw new Error(`${fieldPath} must be an empty object`);
463
+ return;
464
+ }
453
465
  if (fieldPath === 'active_baseagent') {
454
466
  if (!BASEAGENT_SET.has(String(value)))
455
467
  throw new Error(`${fieldPath} is invalid`);
@@ -510,8 +522,7 @@ export function validateBasicSettingsChange(file, change) {
510
522
  else
511
523
  validateDefaultsValue(change.path, change.value);
512
524
  }
513
- function validateBaseagentAvailability(config, changes) {
514
- const details = detectBaseAgentDetails(config);
525
+ function validateBaseagentAvailability(changes, details) {
515
526
  const detected = new Set(details.filter(item => item.detected).map(item => item.id));
516
527
  const configured = new Set(details.filter(item => item.configured).map(item => item.id));
517
528
  for (const change of changes) {
@@ -522,6 +533,10 @@ function validateBaseagentAvailability(config, changes) {
522
533
  if (parsed && !detected.has(parsed.baseagent) && !configured.has(parsed.baseagent)) {
523
534
  throw Object.assign(new Error(`Base Agent is not available on this machine: ${parsed.baseagent}`), { statusCode: 400 });
524
535
  }
536
+ const block = baseagentBlockPath(change.path);
537
+ if (block && change.op === 'set' && !detected.has(block) && !configured.has(block)) {
538
+ throw Object.assign(new Error(`Base Agent is not available on this machine: ${block}`), { statusCode: 400 });
539
+ }
525
540
  }
526
541
  }
527
542
  function setAtPath(root, segments, value) {
@@ -675,7 +690,7 @@ async function readSnapshot(options = {}) {
675
690
  },
676
691
  secrets,
677
692
  secretRefs,
678
- baseagents: detectBaseAgentDetails(rawDefaults),
693
+ baseagents: await detectBaseAgentDetails(rawDefaults),
679
694
  logTypes: logTypeStats.types,
680
695
  logTypeCounts: logTypeStats.counts,
681
696
  };
@@ -779,6 +794,8 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
779
794
  changes.forEach(change => validateBasicSettingsChange(file, change));
780
795
  const modules = await getConfigManager();
781
796
  const target = file === 'daemon' ? modules.ConfigTarget.Process : modules.ConfigTarget.Defaults;
797
+ const currentConfig = modules.read(target, undefined, { cache: false, expand: false }) ?? {};
798
+ const detection = file === 'defaults' ? await detectBaseAgentDetails(currentConfig) : [];
782
799
  const mutation = modules.mutateConfig(target, undefined, config => {
783
800
  if (basicSettingsRevision(config) !== body.revision) {
784
801
  throw Object.assign(new Error('Configuration changed after it was loaded. Reload and try again.'), {
@@ -786,7 +803,7 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
786
803
  });
787
804
  }
788
805
  if (file === 'defaults')
789
- validateBaseagentAvailability(config, changes);
806
+ validateBaseagentAvailability(changes, detection);
790
807
  applyBasicSettingsChanges(config, file, changes);
791
808
  const errors = modules.validateConfig(target, config);
792
809
  if (errors.length > 0) {
@@ -32,21 +32,27 @@ export function normalizeModelEntries(models) {
32
32
  owned_by: model.owned_by,
33
33
  created: model.created,
34
34
  family: inferModelFamily(id),
35
- status: isAlias ? 'alias' : 'available',
35
+ status: isAlias ? 'alias' : model.verification || 'unverified',
36
+ verification: model.verification,
37
+ reasoningLevels: model.reasoningLevels,
38
+ aliasFor: model.aliasFor,
36
39
  isAlias
37
40
  });
38
41
  }
39
42
  return out;
40
43
  }
41
- export async function getModelCatalogSnapshot(baseagent = 'claude') {
44
+ export async function getModelCatalogSnapshot(baseagent = 'claude', self, refresh = false) {
42
45
  const { getCatalog } = await getCatalogModule();
43
- const catalog = await getCatalog(undefined, baseagent);
46
+ const catalog = await getCatalog(self, baseagent, refresh);
44
47
  const normalized = normalizeModelEntries(catalog.models || []);
45
48
  return {
46
49
  models: normalized,
47
50
  catalogModels: normalized,
48
51
  source: catalog.source || 'unknown',
49
- lastUpdate: new Date().toISOString()
52
+ lastUpdate: catalog.fetchedAt,
53
+ revision: catalog.revision,
54
+ freshness: catalog.freshness,
55
+ diagnostics: catalog.diagnostics,
50
56
  };
51
57
  }
52
58
  function sendJson(res, status, payload) {
@@ -58,11 +64,12 @@ export async function handleModelsApi(req, res) {
58
64
  const url = new URL(req.url || '', 'http://localhost');
59
65
  if (req.method === 'GET' && url.pathname === '/api/models/catalog') {
60
66
  const baseagent = url.searchParams.get('baseagent') || 'claude';
61
- if (!/^[a-z0-9_-]+$/i.test(baseagent)) {
67
+ if (!['claude', 'codex', 'ecagent', 'gemini'].includes(baseagent)) {
62
68
  sendJson(res, 400, { success: false, error: 'Invalid baseagent' });
63
69
  return;
64
70
  }
65
- const data = await getModelCatalogSnapshot(baseagent);
71
+ const self = url.searchParams.get('aid') || url.searchParams.get('self') || undefined;
72
+ const data = await getModelCatalogSnapshot(baseagent, self, url.searchParams.get('refresh') === 'true');
66
73
  sendJson(res, 200, { success: true, data });
67
74
  return;
68
75
  }
@@ -14,6 +14,7 @@
14
14
  import fs from 'node:fs';
15
15
  import { resolvePaths } from '../paths.js';
16
16
  import { ipcQuery } from '../ipc-client.js';
17
+ import crypto from 'node:crypto';
17
18
  function processGroupKey(process) {
18
19
  return `${process.kind}\u0000${process.name}`;
19
20
  }
@@ -192,9 +193,9 @@ function sampleNetworkRate(ts) {
192
193
  previousNetworkSample = { ...totals, ts };
193
194
  return latestNetworkRate;
194
195
  }
195
- async function fetchSnapshot() {
196
+ async function fetchSnapshot(leaseId) {
196
197
  const p = resolvePaths();
197
- const resp = await ipcQuery(p.socket, { type: 'monitor-snapshot' }, 3000);
198
+ const resp = await ipcQuery(p.socket, { type: 'monitor-snapshot', ...(leaseId ? { leaseId } : {}) }, 3000);
198
199
  // resp === null → daemon 离线或 socket 不可达;resp.ok === false → 命令不受支持(旧 daemon)
199
200
  if (!resp?.ok) {
200
201
  return { daemonRunning: resp !== null, snapshot: null, history: { fine: [], mid: [], coarse: [] } };
@@ -237,8 +238,24 @@ async function fetchSnapshot() {
237
238
  }
238
239
  export const monitorSource = {
239
240
  kind: 'monitor',
240
- async snapshot() {
241
- return fetchSnapshot();
241
+ async activate(params) {
242
+ const leaseId = crypto.randomUUID();
243
+ const p = resolvePaths();
244
+ const response = await ipcQuery(p.socket, { type: 'monitor-subscribe', leaseId }, 3000);
245
+ if (!response?.ok)
246
+ throw new Error('daemon rejected monitor subscription');
247
+ params.__monitorLeaseId = leaseId;
248
+ let released = false;
249
+ return () => {
250
+ if (released)
251
+ return;
252
+ released = true;
253
+ delete params.__monitorLeaseId;
254
+ void ipcQuery(p.socket, { type: 'monitor-unsubscribe', leaseId }, 3000);
255
+ };
256
+ },
257
+ async snapshot(params) {
258
+ return fetchSnapshot(params?.__monitorLeaseId);
242
259
  },
243
260
  subscribe(_params, push) {
244
261
  let lastJson = '';
@@ -247,7 +264,7 @@ export const monitorSource = {
247
264
  if (stopped)
248
265
  return;
249
266
  try {
250
- const snap = await fetchSnapshot();
267
+ const snap = await fetchSnapshot(_params.__monitorLeaseId);
251
268
  const json = JSON.stringify(snap);
252
269
  if (json !== lastJson) {
253
270
  lastJson = json;
@@ -1,8 +1,17 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolvePaths } from '../paths.js';
4
- import { getSessionsAunDir, listPeers, readJsonFile, readMessages, } from '../fs-utils.js';
4
+ import { getSessionsAunDir, isTransientProtocolMessage, readJsonFile, readAllJsonlLines, scanChatDirs, } from '../fs-utils.js';
5
5
  import { resolveParentDistModule, toFileUrl } from './parent-package.js';
6
+ import crypto from 'node:crypto';
7
+ function ecwebAuditContext(auth, aid, operation) {
8
+ return {
9
+ ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
10
+ targetAgentAid: aid,
11
+ endpoint: auth.endpoint ?? 'ECWeb relation roles',
12
+ logicalToolName: operation,
13
+ };
14
+ }
6
15
  async function getParentModules() {
7
16
  const configPath = resolveParentDistModule('config', 'config-manager.js');
8
17
  const rolesPath = resolveParentDistModule('config', 'roles.js');
@@ -116,6 +125,7 @@ async function authorizeRoleAssignmentRead(modules, aid, auth) {
116
125
  reason: decision.allow ? undefined : decision.reason,
117
126
  matchedRule: decision.matchedRule,
118
127
  argsSummary: { self: aid },
128
+ ...ecwebAuditContext(auth, aid, 'role.relation.read'),
119
129
  });
120
130
  if (decision.allow)
121
131
  return { allow: true, actorRole: actor.actorRole };
@@ -150,6 +160,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
150
160
  code: 'IMMUTABLE_STATIC_OWNER',
151
161
  reason,
152
162
  argsSummary: { ...args, targetRole },
163
+ ...ecwebAuditContext(auth, aid, operation),
153
164
  });
154
165
  return {
155
166
  allow: false,
@@ -194,6 +205,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
194
205
  reason: decision.reason,
195
206
  matchedRule: decision.matchedRule,
196
207
  argsSummary: { ...args, targetRole },
208
+ ...ecwebAuditContext(auth, aid, operation),
197
209
  });
198
210
  return {
199
211
  allow: false,
@@ -227,6 +239,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
227
239
  reason,
228
240
  matchedRule: decision.matchedRule,
229
241
  argsSummary: { ...args, targetRole },
242
+ ...ecwebAuditContext(auth, aid, operation),
230
243
  });
231
244
  return {
232
245
  allow: false,
@@ -251,6 +264,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
251
264
  decision: 'allow',
252
265
  matchedRule: decision.matchedRule,
253
266
  argsSummary: { ...args, targetRole },
267
+ ...ecwebAuditContext(auth, aid, operation),
254
268
  });
255
269
  return { allow: true, actorRole };
256
270
  }
@@ -323,12 +337,6 @@ function relationTargetFromId(raw) {
323
337
  }
324
338
  return { peerKey: formatPeerKey('aun', value), channelId: value };
325
339
  }
326
- function readActive(aunDir, aid, conversationId) {
327
- return readJsonFile(path.join(aunDir, encodeSegmentForRoleSource(aid), encodeSegmentForRoleSource(conversationId), 'active.json'));
328
- }
329
- function encodeSegmentForRoleSource(s) {
330
- return s.replace(/[<>:"/\\|?*\x00-\x1F%]/g, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
331
- }
332
340
  function frontmatterValue(frontmatter, key) {
333
341
  const re = new RegExp(`^${key}:\\s*(?:"([^"]*)"|'([^']*)'|([^\\r\\n#]*))\\s*(?:#.*)?$`, 'm');
334
342
  const match = frontmatter.match(re);
@@ -380,6 +388,20 @@ function createLocalAgentMdLookup() {
380
388
  return info;
381
389
  };
382
390
  }
391
+ function readPeerVerificationStatus(selfAid, peerId) {
392
+ if (!selfAid || !peerId)
393
+ return 'unknown';
394
+ const peerKey = formatPeerKey('aun', peerId);
395
+ const identity = readJsonFile(path.join(resolvePaths().root, 'agents', selfAid, 'relations', peerKey, 'peer-identity.json'));
396
+ const raw = String(identity?.source ?? identity?.verifyStatus ?? identity?.verify_status ?? '').trim().toLowerCase();
397
+ if (raw === 'agentmd' || raw === 'verified')
398
+ return 'agentmd';
399
+ if (raw === 'agentmd-unverified' || raw === 'unverified')
400
+ return 'agentmd-unverified';
401
+ if (raw === 'invalid')
402
+ return 'invalid';
403
+ return 'unknown';
404
+ }
383
405
  function latestMessageAt(messages) {
384
406
  let lastAt = 0;
385
407
  for (const message of messages) {
@@ -418,7 +440,7 @@ function firstMessagePeerType(messages, peerId) {
418
440
  function conversationSortName(seed) {
419
441
  return (seed.name || seed.peerAid || seed.groupId || seed.conversationId).toLowerCase();
420
442
  }
421
- function groupMembersFromMessages(selfAid, messages, localAgentMdLookup) {
443
+ function groupMembersFromMessages(selfAid, messages, localAgentMdLookup, peerVerificationLookup) {
422
444
  const members = new Map();
423
445
  for (const message of messages) {
424
446
  const sender = message.from;
@@ -448,6 +470,7 @@ function groupMembersFromMessages(selfAid, messages, localAgentMdLookup) {
448
470
  const localInfo = localAgentMdLookup(member.peerId);
449
471
  member.peerName = localInfo?.name ?? member.peerName;
450
472
  member.peerType = localInfo?.peerType ?? member.peerType;
473
+ member.verifyStatus = peerVerificationLookup(selfAid, member.peerId);
451
474
  }
452
475
  return [...members.values()].sort((a, b) => {
453
476
  const lastDiff = (b.lastAt ?? 0) - (a.lastAt ?? 0);
@@ -456,7 +479,7 @@ function groupMembersFromMessages(selfAid, messages, localAgentMdLookup) {
456
479
  return (a.peerName || a.peerAid).localeCompare(b.peerName || b.peerAid);
457
480
  });
458
481
  }
459
- export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () => undefined) {
482
+ export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () => undefined, peerVerificationLookup = readPeerVerificationStatus) {
460
483
  const seeds = [];
461
484
  for (const peer of peerInfos) {
462
485
  const messages = peer.messages || [];
@@ -478,7 +501,7 @@ export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () =
478
501
  lastAt,
479
502
  inbound: counts.inbound,
480
503
  outbound: counts.outbound,
481
- members: groupMembersFromMessages(aid, messages, localAgentMdLookup),
504
+ members: groupMembersFromMessages(aid, messages, localAgentMdLookup, peerVerificationLookup),
482
505
  });
483
506
  continue;
484
507
  }
@@ -499,6 +522,7 @@ export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () =
499
522
  peerKey: peerId,
500
523
  peerName,
501
524
  peerType,
525
+ verifyStatus: peerVerificationLookup(aid, peerId),
502
526
  lastAt,
503
527
  inbound: counts.inbound,
504
528
  outbound: counts.outbound,
@@ -515,13 +539,20 @@ export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () =
515
539
  }
516
540
  function loadConversationSeeds(aid) {
517
541
  const aunDir = getSessionsAunDir();
542
+ const peerInfoById = new Map();
543
+ for (const chat of scanChatDirs(aunDir).filter(item => item.channelType === 'aun' && item.selfAID === aid)) {
544
+ const current = peerInfoById.get(chat.channelId) ?? { conversationId: chat.channelId, messages: [] };
545
+ const active = readJsonFile(path.join(chat.dirPath, 'active.json'));
546
+ if (active && (!current.active || Number(active.updatedAt || 0) >= Number(current.active.updatedAt || 0))) {
547
+ current.active = active;
548
+ }
549
+ current.messages.push(...readAllJsonlLines(path.join(chat.dirPath, 'messages.jsonl'))
550
+ .filter(message => !isTransientProtocolMessage(message)));
551
+ peerInfoById.set(chat.channelId, current);
552
+ }
518
553
  const localAgentMdLookup = createLocalAgentMdLookup();
519
- const peerInfos = listPeers(aunDir, aid).map(conversationId => ({
520
- conversationId,
521
- active: readActive(aunDir, aid, conversationId),
522
- messages: readMessages(aunDir, aid, conversationId),
523
- }));
524
- return buildConversationSeeds(aid, peerInfos, localAgentMdLookup);
554
+ const peerInfos = [...peerInfoById.values()];
555
+ return buildConversationSeeds(aid, peerInfos, localAgentMdLookup, readPeerVerificationStatus);
525
556
  }
526
557
  function readRelationConfig(config, aid, peerKey) {
527
558
  return config.read(config.ConfigTarget.Relation, { self: aid, peerKey }, { cache: true }) || {};
@@ -648,6 +679,11 @@ function listRelationRoleAssignments(config, aid) {
648
679
  return out;
649
680
  }
650
681
  async function buildSnapshot(auth = {}) {
682
+ auth = {
683
+ ...auth,
684
+ ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
685
+ endpoint: auth.endpoint ?? 'WS snapshot:roles',
686
+ };
651
687
  const modules = await getParentModules();
652
688
  const agents = await getAgentsFromIpc();
653
689
  const snapshotAgents = [];
@@ -678,7 +714,11 @@ export const relationRolesSource = {
678
714
  kind: 'roles',
679
715
  async snapshot(params = {}) {
680
716
  try {
681
- return await buildSnapshot(params.__menuAuth ?? {});
717
+ return await buildSnapshot({
718
+ ...(params.__menuAuth ?? {}),
719
+ ecwebRequestId: params.__ecwebRequestId,
720
+ endpoint: params.__ecwebEndpoint,
721
+ });
682
722
  }
683
723
  catch (err) {
684
724
  return { agents: [], conversations: [], error: String(err) };
@@ -765,6 +805,11 @@ function deleteScopedAssignment(configMod, aid, body, fallbackTargetId) {
765
805
  export async function handleRelationRolesApi(req, res, auth = {}) {
766
806
  try {
767
807
  const urlPath = (req.url || '').split('?')[0];
808
+ auth = {
809
+ ...auth,
810
+ ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
811
+ endpoint: auth.endpoint ?? `${req.method ?? 'UNKNOWN'} ${urlPath}`,
812
+ };
768
813
  const modules = await getParentModules();
769
814
  if (req.method === 'GET' && urlPath.startsWith('/api/roles/agent/')) {
770
815
  const aid = decodeURIComponent(urlPath.split('/').filter(Boolean).pop() || '');
@@ -802,6 +847,11 @@ export async function handleRelationRolesApi(req, res, auth = {}) {
802
847
  export async function handlePeerRoleApi(req, res, auth = {}) {
803
848
  try {
804
849
  const urlPath = (req.url || '').split('?')[0];
850
+ auth = {
851
+ ...auth,
852
+ ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
853
+ endpoint: auth.endpoint ?? `${req.method ?? 'UNKNOWN'} ${urlPath}`,
854
+ };
805
855
  const target = assignmentFromPath(urlPath);
806
856
  if (!target)
807
857
  return sendJson(res, 400, { error: 'missing aid or target id' });