evolcore 0.0.20 → 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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -9,6 +9,7 @@ import { HANDOFF_SCHEMA_VERSION, HANDOFF_QUERY_DEFAULT_LIMIT, HANDOFF_QUERY_MAX_
9
9
  import { normalizeCausation } from '../causation/context.js';
10
10
  const SAFE_ID_RE = /^[A-Za-z0-9._-]+$/;
11
11
  export const HANDOFF_ID_RE = /^h-[A-Za-z0-9._-]+$/;
12
+ const HANDOFF_SNAPSHOT_FILE_RE = /^(h-[A-Za-z0-9._-]+)\.json(?:__?)?$/;
12
13
  const TERMINAL_HANDOFF_STATES = new Set(['failed', 'completed', 'discarded']);
13
14
  function safeSegment(value, field) {
14
15
  if (!value || !SAFE_ID_RE.test(value) || value === '.' || value === '..') {
@@ -55,13 +56,16 @@ export class HandoffStore {
55
56
  ? path.join(this.rootDir, aid, 'data', 'handoff')
56
57
  : path.join(this.rootDir, aid);
57
58
  }
58
- handoffsDir(selfAid) {
59
- return path.join(this.agentDir(selfAid), 'handoffs');
60
- }
61
59
  instancePath(selfAid, handoffId) {
62
60
  if (!HANDOFF_ID_RE.test(handoffId))
63
61
  throw new Error('invalid handoff_id');
64
- return path.join(this.handoffsDir(selfAid), safeSegment(handoffId, 'handoff_id'), 'handoff.json');
62
+ return path.join(this.agentDir(selfAid), `${safeSegment(handoffId, 'handoff_id')}.json`);
63
+ }
64
+ /** Read the pre-flat layout during the transition to the flat store. */
65
+ legacyInstancePath(selfAid, handoffId) {
66
+ if (!HANDOFF_ID_RE.test(handoffId))
67
+ throw new Error('invalid handoff_id');
68
+ return path.join(this.agentDir(selfAid), 'handoffs', safeSegment(handoffId, 'handoff_id'), 'handoff.json');
65
69
  }
66
70
  historyPath(selfAid) {
67
71
  return path.join(this.agentDir(selfAid), 'history.jsonl');
@@ -147,7 +151,8 @@ export class HandoffStore {
147
151
  get(selfAid, handoffId) {
148
152
  if (!HANDOFF_ID_RE.test(handoffId))
149
153
  return null;
150
- const parsed = atomicReadJson(this.instancePath(selfAid, handoffId));
154
+ const parsed = atomicReadJson(this.instancePath(selfAid, handoffId))
155
+ ?? atomicReadJson(this.legacyInstancePath(selfAid, handoffId));
151
156
  if (!parsed)
152
157
  return null;
153
158
  if (parsed.schema_version !== HANDOFF_SCHEMA_VERSION || parsed.handoff_id !== handoffId) {
@@ -156,14 +161,32 @@ export class HandoffStore {
156
161
  return parsed;
157
162
  }
158
163
  list(selfAid) {
159
- const dir = this.handoffsDir(selfAid);
164
+ const dir = this.agentDir(selfAid);
160
165
  if (!fs.existsSync(dir))
161
166
  return [];
162
- const result = [];
167
+ const handoffIds = new Set();
163
168
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
164
- if (!entry.isDirectory() || !HANDOFF_ID_RE.test(entry.name))
169
+ if (!entry.isFile())
165
170
  continue;
166
- const instance = this.get(selfAid, entry.name);
171
+ const match = HANDOFF_SNAPSHOT_FILE_RE.exec(entry.name);
172
+ if (match && HANDOFF_ID_RE.test(match[1]))
173
+ handoffIds.add(match[1]);
174
+ }
175
+ // Keep nested snapshots visible; a later flat snapshot takes precedence.
176
+ const legacyDir = path.join(dir, 'handoffs');
177
+ try {
178
+ for (const entry of fs.readdirSync(legacyDir, { withFileTypes: true })) {
179
+ if (entry.isDirectory() && HANDOFF_ID_RE.test(entry.name))
180
+ handoffIds.add(entry.name);
181
+ }
182
+ }
183
+ catch (error) {
184
+ if (error.code !== 'ENOENT')
185
+ throw error;
186
+ }
187
+ const result = [];
188
+ for (const handoffId of handoffIds) {
189
+ const instance = this.get(selfAid, handoffId);
167
190
  if (instance)
168
191
  result.push(instance);
169
192
  }
@@ -1,5 +1,6 @@
1
1
  import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveEcagentConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
2
2
  import { buildModelRequestHeaders } from '../../agents/request-identity.js';
3
+ import { appendRequestQueryParams } from '../../agents/request-identity.js';
3
4
  function apiEndpoint(baseUrl, resource) {
4
5
  const configured = baseUrl?.trim();
5
6
  if (!configured)
@@ -11,14 +12,6 @@ function apiEndpoint(baseUrl, resource) {
11
12
  : `${pathname}/v1/${resource}`;
12
13
  return parsed.toString();
13
14
  }
14
- function withQueryParams(url, queryParams) {
15
- if (!queryParams || Object.keys(queryParams).length === 0)
16
- return url;
17
- const parsed = new URL(url);
18
- for (const [name, value] of Object.entries(queryParams))
19
- parsed.searchParams.set(name, value);
20
- return parsed.toString();
21
- }
22
15
  async function parseError(response) {
23
16
  let raw = '';
24
17
  try {
@@ -41,7 +34,7 @@ export class AnthropicTextInferenceProvider {
41
34
  this.config = config;
42
35
  }
43
36
  async completeText(request) {
44
- const response = await fetch(withQueryParams(apiEndpoint(this.config.baseUrl, 'messages'), this.config.queryParams), {
37
+ const response = await fetch(appendRequestQueryParams(apiEndpoint(this.config.baseUrl, 'messages'), this.config.queryParams), {
45
38
  method: 'POST',
46
39
  signal: request.signal,
47
40
  headers: { ...this.headers(), ...this.config.headers },
@@ -82,7 +75,7 @@ export class OpenAITextInferenceProvider {
82
75
  this.config = config;
83
76
  }
84
77
  async completeText(request) {
85
- const response = await fetch(withQueryParams(apiEndpoint(this.config.baseUrl, 'responses'), this.config.queryParams), {
78
+ const response = await fetch(appendRequestQueryParams(apiEndpoint(this.config.baseUrl, 'responses'), this.config.queryParams), {
86
79
  method: 'POST',
87
80
  signal: request.signal,
88
81
  headers: {
@@ -118,20 +111,20 @@ export class OpenAITextInferenceProvider {
118
111
  export class EcagentTextInferenceProvider {
119
112
  config;
120
113
  baseagent = 'ecagent';
121
- constructor(config) {
114
+ constructor(config, agentAid) {
122
115
  this.config = config;
123
116
  this.config = {
124
117
  ...config,
125
118
  headers: buildModelRequestHeaders({
126
119
  baseagent: 'ecagent',
127
120
  baseUrl: config.baseUrl,
128
- agentAid: config.evolcoreAgentAid,
121
+ agentAid,
129
122
  configuredHeaders: config.headers,
130
123
  }),
131
124
  };
132
125
  }
133
126
  async completeText(request) {
134
- const response = await fetch(apiEndpoint(this.config.baseUrl, 'chat/completions'), {
127
+ const response = await fetch(appendRequestQueryParams(apiEndpoint(this.config.baseUrl, 'chat/completions'), this.config.queryParams), {
135
128
  method: 'POST',
136
129
  signal: request.signal,
137
130
  headers: {
@@ -190,10 +183,9 @@ export function createTextInferenceProvider(baseagent, config) {
190
183
  try {
191
184
  const override = {
192
185
  ...(config.baseagents?.ecagent ?? {}),
193
- evolcoreAgentAid: config.aid,
194
186
  };
195
187
  const resolved = resolveEcagentConfig({ agents: { ecagent: override } }, override);
196
- return new EcagentTextInferenceProvider(resolved);
188
+ return new EcagentTextInferenceProvider(resolved, config.aid);
197
189
  }
198
190
  catch {
199
191
  return undefined;
@@ -1,23 +1,7 @@
1
1
  import { logger } from '../../utils/logger.js';
2
2
  import { summarizeToolInput } from '../../utils/tool-summary.js';
3
3
  import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
4
- import fs from 'fs';
5
- import path from 'path';
6
- import { resolvePaths } from '../../paths.js';
7
4
  import { DEFAULT_FLUSH_DELAY_MS } from '../../types.js';
8
- let diagStream = null;
9
- function getDiagStream() {
10
- if (!diagStream) {
11
- const logDir = resolvePaths().logs;
12
- diagStream = fs.createWriteStream(path.join(logDir, 'im-renderer-diag.log'), { flags: 'a' });
13
- }
14
- return diagStream;
15
- }
16
- function diag(instanceId, action, meta = {}) {
17
- const line = JSON.stringify({ ts: new Date().toISOString(), id: instanceId, action, ...meta });
18
- getDiagStream().write(line + '\n');
19
- }
20
- let instanceCounter = 0;
21
5
  export class IMRenderer {
22
6
  opts;
23
7
  itemsQueue = [];
@@ -28,8 +12,6 @@ export class IMRenderer {
28
12
  sentContent = false;
29
13
  flushCount = 0;
30
14
  messageTimestamps = [];
31
- instanceId;
32
- diagEnabled;
33
15
  /** 串行发送队列:保证消息按序到达 */
34
16
  sendChain = Promise.resolve();
35
17
  /** proactive:是否已发过 text 文本(用于去重 complete.result) */
@@ -38,16 +20,6 @@ export class IMRenderer {
38
20
  syntheticCallSeq = 0;
39
21
  constructor(opts) {
40
22
  this.opts = opts;
41
- this.diagEnabled = opts.diagEnabled ?? false;
42
- this.instanceId = `R${++instanceCounter}`;
43
- if (this.diagEnabled) {
44
- diag(this.instanceId, 'created', {
45
- chatmode: opts.envelope.chatmode,
46
- flushDelay: opts.flushDelay,
47
- suppressActivityItems: this.suppressActivityItems(),
48
- suppressIntermediateText: this.suppressIntermediateText(),
49
- });
50
- }
51
23
  }
52
24
  suppressActivityItems() {
53
25
  return this.opts.suppressActivityItems ?? this.opts.suppressActivities ?? false;
@@ -138,10 +110,14 @@ export class IMRenderer {
138
110
  this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
139
111
  const payload = { kind: 'result.text', text: rawText, isFinal: false };
140
112
  this.sentContent = true;
141
- this.sendChain = this.sendChain
142
- .then(() => this.opts.send(payload))
143
- .catch(e => logger.warn('[IMRenderer] flushText send failed:', e));
144
- await this.sendChain;
113
+ try {
114
+ await this.sendPayload(payload);
115
+ }
116
+ catch (error) {
117
+ // A failed durable enqueue must remain retryable by the final flush.
118
+ this.textBuffer = rawText + this.textBuffer;
119
+ throw error;
120
+ }
145
121
  this.lastFlush = Date.now();
146
122
  this.flushCount++;
147
123
  }
@@ -195,13 +171,6 @@ export class IMRenderer {
195
171
  this.textBuffer += text;
196
172
  this.allText += text;
197
173
  this.messageTimestamps.push(Date.now());
198
- if (this.diagEnabled) {
199
- diag(this.instanceId, 'addText', {
200
- len: text.length,
201
- preview: text.substring(0, 60),
202
- bufLen: this.textBuffer.length,
203
- });
204
- }
205
174
  this.scheduleFlush();
206
175
  }
207
176
  /** 添加工具调用 */
@@ -219,8 +188,6 @@ export class IMRenderer {
219
188
  text: descText,
220
189
  });
221
190
  this.messageTimestamps.push(Date.now());
222
- if (this.diagEnabled)
223
- diag(this.instanceId, 'addToolCall', { name, callId });
224
191
  this.scheduleFlush();
225
192
  }
226
193
  /** 添加工具结果 */
@@ -241,8 +208,6 @@ export class IMRenderer {
241
208
  ...(descText !== undefined && { text: descText }),
242
209
  });
243
210
  this.messageTimestamps.push(Date.now());
244
- if (this.diagEnabled)
245
- diag(this.instanceId, 'addToolResult', { name, ok, callId });
246
211
  this.scheduleFlush();
247
212
  }
248
213
  /** 添加进度提示 */
@@ -330,10 +295,7 @@ export class IMRenderer {
330
295
  await this.flushText();
331
296
  const payload = { kind: 'result.text', text, isFinal: false };
332
297
  this.sentContent = true;
333
- this.sendChain = this.sendChain
334
- .then(() => this.opts.send(payload))
335
- .catch(e => logger.warn('[IMRenderer] notice text send failed:', e));
336
- await this.sendChain;
298
+ await this.sendPayload(payload);
337
299
  this.lastFlush = Date.now();
338
300
  this.flushCount++;
339
301
  return true;
@@ -342,10 +304,48 @@ export class IMRenderer {
342
304
  synthCallId() {
343
305
  return `synth-${this.opts.envelope.taskId}-${++this.syntheticCallSeq}`;
344
306
  }
307
+ /** Convert a structured failed receipt into the same error shape as a rejected sender. */
308
+ receiptError(receipt) {
309
+ return Object.assign(new Error(receipt.error), {
310
+ code: receipt.code,
311
+ outboxId: receipt.outboxId,
312
+ operationId: receipt.operationId,
313
+ status: receipt.status,
314
+ });
315
+ }
316
+ isOutboxFull(error) {
317
+ const value = error && typeof error === 'object' ? error : {};
318
+ const cause = value.cause && typeof value.cause === 'object'
319
+ ? value.cause
320
+ : undefined;
321
+ return value.code === 'OUTBOX_FULL' || cause?.code === 'OUTBOX_FULL';
322
+ }
323
+ /**
324
+ * Serialize durable sends while keeping the chain usable after a rejected
325
+ * operation. Activity queue saturation is best-effort: it is logged, but
326
+ * must not prevent the final response from being sent.
327
+ */
328
+ sendPayload(payload) {
329
+ const operation = this.sendChain.then(async () => {
330
+ const receipt = await this.opts.send(payload);
331
+ if (receipt?.status === 'failed') {
332
+ throw this.receiptError(receipt);
333
+ }
334
+ });
335
+ const handled = operation.catch(error => {
336
+ if (payload.kind === 'activity.batch' && this.isOutboxFull(error)) {
337
+ logger.warn(`[IMRenderer] activity outbox full; continuing without activity batch: ${error instanceof Error ? error.message : String(error)}`);
338
+ return;
339
+ }
340
+ throw error;
341
+ });
342
+ // A failed operation must not poison later sends. The caller still gets
343
+ // the rejection through `handled`, while the internal tail is recovered.
344
+ this.sendChain = handled.catch(() => { });
345
+ return handled;
346
+ }
345
347
  scheduleFlush() {
346
348
  if (this.timer) {
347
- if (this.diagEnabled)
348
- diag(this.instanceId, 'scheduleFlush:skip', { reason: 'timer_exists' });
349
349
  return;
350
350
  }
351
351
  const interval = this.opts.flushDelay ?? DEFAULT_FLUSH_DELAY_MS;
@@ -364,15 +364,11 @@ export class IMRenderer {
364
364
  }
365
365
  const elapsed = Date.now() - this.lastFlush;
366
366
  const delay = Math.max(0, targetDelay - elapsed);
367
- if (this.diagEnabled) {
368
- diag(this.instanceId, 'scheduleFlush:set', {
369
- flushCount: this.flushCount,
370
- targetDelay,
371
- elapsed,
372
- actualDelay: delay,
367
+ this.timer = setTimeout(() => {
368
+ void this.flushInternal().catch(error => {
369
+ logger.warn(`[IMRenderer] scheduled flush failed: ${error instanceof Error ? error.message : String(error)}`);
373
370
  });
374
- }
375
- this.timer = setTimeout(() => this.flushInternal(), delay);
371
+ }, delay);
376
372
  }
377
373
  calculateDynamicDelay(interval) {
378
374
  const recent = this.messageTimestamps.slice(-10);
@@ -402,13 +398,14 @@ export class IMRenderer {
402
398
  if (this.suppressActivityItems())
403
399
  return;
404
400
  const payload = { kind: 'activity.batch', items: nonThinking };
405
- if (this.diagEnabled)
406
- diag(this.instanceId, 'flushActivitiesOnly', { itemCount: nonThinking.length });
407
401
  this.sentContent = true;
408
- this.sendChain = this.sendChain
409
- .then(() => this.opts.send(payload))
410
- .catch(e => logger.warn('[IMRenderer] activity.batch send failed:', e));
411
- await this.sendChain;
402
+ try {
403
+ await this.sendPayload(payload);
404
+ }
405
+ catch (error) {
406
+ this.itemsQueue = [...nonThinking, ...this.itemsQueue];
407
+ throw error;
408
+ }
412
409
  this.lastFlush = Date.now();
413
410
  this.flushCount++;
414
411
  }
@@ -446,24 +443,20 @@ export class IMRenderer {
446
443
  const finalText = isFinal ? this.textBuffer : '';
447
444
  if (isFinal)
448
445
  this.textBuffer = '';
449
- if (this.diagEnabled) {
450
- diag(this.instanceId, 'flush', {
451
- isFinal,
452
- itemCount: items.length,
453
- finalTextLen: finalText.length,
454
- flushCount: this.flushCount,
455
- sinceLastFlush: Date.now() - this.lastFlush,
456
- });
457
- }
458
446
  // 1. interactive 模式下:不发 text items(由 result.text 统一发送最终文本)
459
447
  const itemsForBatch = this.suppressActivityItems() ? [] : items.filter(it => it.kind !== 'text');
460
448
  if (itemsForBatch.length > 0) {
461
449
  const payload = { kind: 'activity.batch', items: itemsForBatch };
462
450
  this.sentContent = true;
463
- this.sendChain = this.sendChain
464
- .then(() => this.opts.send(payload))
465
- .catch(e => logger.warn('[IMRenderer] activity.batch send failed:', e));
466
- await this.sendChain;
451
+ try {
452
+ await this.sendPayload(payload);
453
+ }
454
+ catch (error) {
455
+ this.itemsQueue = [...itemsForBatch, ...this.itemsQueue];
456
+ if (isFinal && finalText)
457
+ this.textBuffer = finalText + this.textBuffer;
458
+ throw error;
459
+ }
467
460
  this.lastFlush = Date.now();
468
461
  this.flushCount++;
469
462
  }
@@ -481,10 +474,13 @@ export class IMRenderer {
481
474
  this.textBuffer = '';
482
475
  const payload = { kind: 'result.text', text, isFinal: false };
483
476
  this.sentContent = true;
484
- this.sendChain = this.sendChain
485
- .then(() => this.opts.send(payload))
486
- .catch(e => logger.warn('[IMRenderer] timed result.text send failed:', e));
487
- await this.sendChain;
477
+ try {
478
+ await this.sendPayload(payload);
479
+ }
480
+ catch (error) {
481
+ this.textBuffer = text + this.textBuffer;
482
+ throw error;
483
+ }
488
484
  this.lastFlush = Date.now();
489
485
  this.flushCount++;
490
486
  }
@@ -492,10 +488,13 @@ export class IMRenderer {
492
488
  if (isFinal && finalText.length > 0) {
493
489
  const payload = { kind: 'result.text', text: finalText, isFinal: true };
494
490
  this.sentContent = true;
495
- this.sendChain = this.sendChain
496
- .then(() => this.opts.send(payload))
497
- .catch(e => logger.warn('[IMRenderer] result.text send failed:', e));
498
- await this.sendChain;
491
+ try {
492
+ await this.sendPayload(payload);
493
+ }
494
+ catch (error) {
495
+ this.textBuffer = finalText + this.textBuffer;
496
+ throw error;
497
+ }
499
498
  this.lastFlush = Date.now();
500
499
  this.flushCount++;
501
500
  }
@@ -1,5 +1,55 @@
1
1
  import { AccessPolicyError, readAccessPolicy, } from '../../config/access-policy.js';
2
- import { isAcceptedContact, resolveContactView, resolvePrincipal, } from '../../config/contact-book.js';
2
+ import { getContactSnapshot, isAcceptedContact, resolveContactView, resolvePrincipal, } from '../../config/contact-book.js';
3
+ import { isValidAid } from '../../aun/aid/validation.js';
4
+ import { expirePendingContactRequests } from '../../config/contact-request-service.js';
5
+ /** Validate managed private AUN sends against the sender's local contact policy. */
6
+ export async function evaluateOutboundContactAdmission(input) {
7
+ const selfAid = String(input.selfAid || '').trim();
8
+ const targetAid = String(input.targetAid || '').trim();
9
+ if (!isValidAid(selfAid) || !isValidAid(targetAid)) {
10
+ return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: 'A valid Agent AID is required for outbound contact admission' };
11
+ }
12
+ let policy;
13
+ try {
14
+ policy = readAccessPolicy(selfAid);
15
+ }
16
+ catch (error) {
17
+ return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
18
+ }
19
+ if (policy.policyMode !== 'contacts') {
20
+ try {
21
+ if (getContactSnapshot(selfAid).contacts.get(targetAid)?.status === 'blocked') {
22
+ return { allow: false, code: 'CONTACT_BLOCKED', status: 'blocked', error: `目标联系人 ${targetAid} 已被拉黑。请先解除拉黑:ec contact unblock ${targetAid} --self ${selfAid}` };
23
+ }
24
+ }
25
+ catch (error) {
26
+ return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
27
+ }
28
+ return { allow: true, status: policy.policyMode };
29
+ }
30
+ try {
31
+ await expirePendingContactRequests(selfAid);
32
+ const snapshot = getContactSnapshot(selfAid);
33
+ const entry = snapshot.contacts.get(targetAid);
34
+ const view = resolveContactView(selfAid, targetAid);
35
+ if (view.isOwner)
36
+ return { allow: true, status: 'owner' };
37
+ if (entry && (entry.status === undefined || entry.status === 'active'))
38
+ return { allow: true, status: 'active' };
39
+ const status = entry?.status ?? 'absent';
40
+ if (status === 'blocked')
41
+ return { allow: false, code: 'CONTACT_BLOCKED', status, error: `目标联系人 ${targetAid} 已被拉黑。请先解除拉黑:ec contact unblock ${targetAid} --self ${selfAid}` };
42
+ const addCommand = `ec contact add ${targetAid} --self ${selfAid}`;
43
+ if (status === 'pending')
44
+ return { allow: false, code: 'CONTACT_PENDING', status, error: `目标联系人 ${targetAid} 的联系人申请正在审核中,请等待 Owner 通过;如需重新提交,执行:${addCommand}` };
45
+ if (status === 'declined')
46
+ return { allow: false, code: 'CONTACT_DECLINED', status, error: `目标联系人 ${targetAid} 的联系人申请已拒绝或过期,请重新申请:${addCommand}` };
47
+ return { allow: false, code: 'CONTACT_NOT_ACTIVE', status: 'absent', error: `目标 ${targetAid} 不是联系人,请先申请添加:${addCommand}` };
48
+ }
49
+ catch (error) {
50
+ return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
51
+ }
52
+ }
3
53
  export function evaluateInboundAdmissionPreflight(input) {
4
54
  const selfAid = String(input.selfAid || '').trim();
5
55
  const channelType = String(input.channelType || '').trim().toLowerCase();