evolcore 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.
Files changed (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -1,3 +1,4 @@
1
+ import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
4
  import { AgentHarness, JsonlSessionRepo, NodeExecutionEnv, } from '../../ecagent/dist/node.js';
@@ -20,8 +21,6 @@ const PROVIDER_ID = 'evolcore-gateway';
20
21
  const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
21
22
  Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
22
23
  Use the provided tools for filesystem and shell work. Never claim a tool action succeeded unless its result confirms success.`;
23
- const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
24
- const MODEL_LIST_TIMEOUT_MS = 8_000;
25
24
  const MAX_READ_BYTES = 1024 * 1024;
26
25
  const MAX_TOOL_OUTPUT = 200_000;
27
26
  const MAX_SCAN_FILES = 5_000;
@@ -705,7 +704,6 @@ export class EcagentRunner {
705
704
  permissionGateway;
706
705
  sendPromptFn;
707
706
  onCompactStart;
708
- modelCache;
709
707
  constructor(config, callbacks, runtime) {
710
708
  this.config = {
711
709
  ...config,
@@ -745,35 +743,16 @@ export class EcagentRunner {
745
743
  { key: 'bypass', nameZh: '放行', description: '普通审批清单自动执行,强危险操作仍需审批', available: true },
746
744
  ];
747
745
  }
746
+ getModelCatalog() {
747
+ return getRunnerCatalog('ecagent', this.agentAid, this.config);
748
+ }
748
749
  async listModels() {
749
- if (this.modelCache && Date.now() - this.modelCache.fetchedAt < MODEL_CACHE_TTL_MS)
750
- return [...this.modelCache.ids];
751
- const stale = this.modelCache?.ids;
752
- try {
753
- const signal = AbortSignal.timeout(MODEL_LIST_TIMEOUT_MS);
754
- const response = await fetch(appendRequestPath(this.config.baseUrl, 'models', this.config.queryParams), {
755
- headers: {
756
- authorization: `Bearer ${this.config.apiKey}`,
757
- ...this.config.headers,
758
- },
759
- signal,
760
- });
761
- if (!response.ok)
762
- throw new Error(`Failed to list ecagent models: HTTP ${response.status}`);
763
- const payload = await response.json();
764
- const ids = [...new Set((payload.data ?? [])
765
- .map(item => typeof item.id === 'string' ? item.id : '')
766
- .filter(id => id.startsWith('claude-') || (id.startsWith('gpt-') && !id.startsWith('gpt-image-'))))];
767
- if (ids.length === 0)
768
- throw new Error('Gateway returned no supported Claude or GPT models');
769
- this.modelCache = { ids, fetchedAt: Date.now() };
770
- return [...ids];
771
- }
772
- catch (error) {
773
- if (stale && stale.length > 0)
774
- return [...stale];
775
- throw error;
776
- }
750
+ const catalog = await this.getModelCatalog();
751
+ assertCatalogSelectable(catalog);
752
+ return catalog.models.map(m => m.id);
753
+ }
754
+ async getAvailableEfforts(model) {
755
+ return catalogEfforts(await this.getModelCatalog(), model, 'ecagent');
777
756
  }
778
757
  registerStream(key, stream) { this.activeStreams.set(key, stream); }
779
758
  cleanupStream(key) {
@@ -1,3 +1,4 @@
1
+ import { getCatalog, assertCatalogSelectable } from '../core/model/model-catalog.js';
1
2
  /**
2
3
  * Gemini Agent Runner
3
4
  *
@@ -39,11 +40,6 @@ const MIME_EXT = {
39
40
  'image/webp': '.webp',
40
41
  };
41
42
  // ── Gemini 模型列表 ──
42
- const GEMINI_MODELS = [
43
- 'gemini-2.5-pro',
44
- 'gemini-2.5-flash',
45
- 'gemini-2.5-flash-lite',
46
- ];
47
43
  const GEMINI_READONLY_TOOLS = [
48
44
  'read_file', 'list_directory', 'glob', 'grep_search', 'google_web_search',
49
45
  'codebase_investigator', 'cli_help', 'get_internal_docs', 'update_topic', 'complete_task',
@@ -296,7 +292,7 @@ export class GeminiRunner {
296
292
  // ── ModelSwitcher ──
297
293
  setModel(model) { this.model = model; }
298
294
  getModel() { return this.model; }
299
- listModels() { return GEMINI_MODELS; }
295
+ async listModels() { const catalog = await getCatalog(undefined, 'gemini'); assertCatalogSelectable(catalog); return catalog.models.map(m => m.id); }
300
296
  // ── Effort (not applicable) ──
301
297
  setEffort(_effort) { }
302
298
  getEffort() { return undefined; }
@@ -24,6 +24,7 @@ const GROUP_PULL_PAGINATION_PATCH = Symbol('evolcore.groupPullPaginationPatch');
24
24
  * - CLI / ecweb 查询使用新的独立 slot,不能共享 daemon 的 token 或 seq 游标。
25
25
  * - netcheck 使用独立消费者,不得因诊断连接影响 daemon 的入站顺序。
26
26
  * - 群规则同步是短连接 RPC,也必须使用独立 slot,避免触碰 daemon 消费游标。
27
+ * - menu-client 是远程 Control AID 的短连接请求器,使用独立 slot,避免触碰 daemon 消费游标。
27
28
  * - bench 各并发单元用 evolcore-bench-<N>,各自独立隔离键,互不干扰
28
29
  */
29
30
  export const SLOT = {
@@ -32,6 +33,7 @@ export const SLOT = {
32
33
  daemon: 'evolcore daemon',
33
34
  cli: 'evolcore-cli',
34
35
  groupRules: 'evolcore-group-rules',
36
+ menuClient: 'evolcore-menu-client',
35
37
  bench: 'evolcore-bench',
36
38
  netcheck: 'evolcore-netcheck',
37
39
  };
@@ -0,0 +1,272 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { createShortConnection } from './rpc/client.js';
5
+ import { SLOT } from './aid/store.js';
6
+ import { isValidAid } from './aid/validation.js';
7
+ import { resolveRoot } from '../paths.js';
8
+ import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
9
+ import { buildMenuClientProtectedHeaders, parseMenuResponse, validateMenuRequest, } from '../core/command/menu-protocol.js';
10
+ const DEFAULT_TIMEOUT_MS = 15_000;
11
+ const MAX_TIMEOUT_MS = 120_000;
12
+ const POLL_INTERVAL_MS = 200;
13
+ const PULL_LIMIT = 50;
14
+ const LOCK_STALE_MS = 5 * 60_000;
15
+ export class MenuClientError extends Error {
16
+ code;
17
+ data;
18
+ constructor(code, message, data) {
19
+ super(message);
20
+ this.code = code;
21
+ this.data = data;
22
+ this.name = 'MenuClientError';
23
+ }
24
+ }
25
+ async function callClient(conn, method, params) {
26
+ try {
27
+ return await conn.call(method, params);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof MenuClientError)
31
+ throw error;
32
+ const code = String(error?.code ?? '').toLowerCase();
33
+ const message = error?.message ?? String(error);
34
+ const timedOut = code.includes('timeout') || code.includes('timed_out') || /timed?\s*out|timeout/i.test(String(message));
35
+ throw new MenuClientError(timedOut ? 'TIMEOUT' : 'RPC_FAILED', String(message));
36
+ }
37
+ }
38
+ function normalizeTimeout(value) {
39
+ const timeout = value ?? DEFAULT_TIMEOUT_MS;
40
+ if (!Number.isFinite(timeout) || timeout < 1_000 || timeout > MAX_TIMEOUT_MS) {
41
+ throw new MenuClientError('INVALID_ARGUMENT', `timeout must be between 1s and ${MAX_TIMEOUT_MS / 1000}s`);
42
+ }
43
+ return Math.round(timeout);
44
+ }
45
+ function lockPath(aunPath, fromAid) {
46
+ const identity = crypto.createHash('sha256').update(fromAid).digest('hex').slice(0, 16);
47
+ return path.join(aunPath ?? resolveRoot(), 'data', 'instance', `menu-client-${identity}.lock`);
48
+ }
49
+ function acquireClientLock(aunPath, fromAid) {
50
+ const file = lockPath(aunPath, fromAid);
51
+ fs.mkdirSync(path.dirname(file), { recursive: true });
52
+ const token = `${process.pid}:${crypto.randomUUID()}`;
53
+ try {
54
+ const stat = fs.statSync(file);
55
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
56
+ try {
57
+ fs.unlinkSync(file);
58
+ }
59
+ catch { }
60
+ }
61
+ }
62
+ catch { }
63
+ let fd;
64
+ try {
65
+ fd = fs.openSync(file, 'wx', 0o600);
66
+ fs.writeFileSync(fd, token, 'utf8');
67
+ }
68
+ catch (error) {
69
+ try {
70
+ if (fd !== undefined)
71
+ fs.closeSync(fd);
72
+ }
73
+ catch { }
74
+ try {
75
+ if (fs.existsSync(file))
76
+ fs.unlinkSync(file);
77
+ }
78
+ catch { }
79
+ if (error?.code === 'EEXIST')
80
+ throw new MenuClientError('CLIENT_BUSY', 'Another ec menu-client request is using the configured slot');
81
+ throw new MenuClientError('CLIENT_LOCK_FAILED', `Unable to acquire menu-client lock: ${error?.message ?? String(error)}`);
82
+ }
83
+ return () => {
84
+ try {
85
+ const current = fs.readFileSync(file, 'utf8');
86
+ if (current === token)
87
+ fs.unlinkSync(file);
88
+ }
89
+ catch { }
90
+ if (fd !== undefined) {
91
+ try {
92
+ fs.closeSync(fd);
93
+ }
94
+ catch { }
95
+ }
96
+ };
97
+ }
98
+ function normalizeRequest(value) {
99
+ const request = { ...value };
100
+ if (typeof request.id !== 'string' || !request.id.trim())
101
+ request.id = crypto.randomUUID();
102
+ const error = validateMenuRequest(request);
103
+ if (error)
104
+ throw new MenuClientError(error.code, error.message, error.data);
105
+ return request;
106
+ }
107
+ function responseFromMessage(message) {
108
+ const from = typeof message?.from === 'string'
109
+ ? message.from.trim()
110
+ : typeof message?.envelope?.from === 'string' ? message.envelope.from.trim() : '';
111
+ let payload = message?.payload;
112
+ if (typeof payload === 'string') {
113
+ try {
114
+ payload = JSON.parse(payload);
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ }
120
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
121
+ return undefined;
122
+ if (from && typeof payload.type === 'string' && payload.type === 'menu.response') {
123
+ try {
124
+ return parseMenuResponse(payload);
125
+ }
126
+ catch {
127
+ return undefined;
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ async function pollResponse(conn, targetAid, requestId, timeoutMs, initialAfterSeq) {
133
+ const deadline = Date.now() + timeoutMs;
134
+ let afterSeq = initialAfterSeq;
135
+ while (Date.now() < deadline) {
136
+ const result = await callClient(conn, 'message.pull', { after_seq: afterSeq, limit: PULL_LIMIT });
137
+ const messages = Array.isArray(result?.messages) ? result.messages : [];
138
+ let maxSeq = afterSeq;
139
+ for (const message of messages) {
140
+ const seq = Number(message?.seq);
141
+ if (Number.isSafeInteger(seq) && seq > maxSeq)
142
+ maxSeq = seq;
143
+ const response = responseFromMessage(message);
144
+ const messageFrom = typeof message?.from === 'string'
145
+ ? message.from.trim()
146
+ : typeof message?.envelope?.from === 'string' ? message.envelope.from.trim() : '';
147
+ if (response && messageFrom === targetAid && response.id === requestId) {
148
+ if (Number.isSafeInteger(maxSeq) && maxSeq > afterSeq) {
149
+ try {
150
+ await callClient(conn, 'message.ack', { seq: maxSeq });
151
+ }
152
+ catch (error) {
153
+ throw new MenuClientError('ACK_FAILED', error?.message ?? String(error));
154
+ }
155
+ }
156
+ return response;
157
+ }
158
+ }
159
+ if (Number.isSafeInteger(maxSeq) && maxSeq > afterSeq) {
160
+ afterSeq = maxSeq;
161
+ try {
162
+ await callClient(conn, 'message.ack', { seq: afterSeq });
163
+ }
164
+ catch (error) {
165
+ throw new MenuClientError('ACK_FAILED', error?.message ?? String(error));
166
+ }
167
+ }
168
+ const waitMs = Math.min(POLL_INTERVAL_MS, Math.max(1, deadline - Date.now()));
169
+ await new Promise(resolve => setTimeout(resolve, waitMs));
170
+ }
171
+ throw new MenuClientError('TIMEOUT', `Timed out waiting for Menu response ${requestId}`);
172
+ }
173
+ async function drainBaseline(conn) {
174
+ let afterSeq = 0;
175
+ for (let page = 0; page < 200; page += 1) {
176
+ const result = await callClient(conn, 'message.pull', { after_seq: afterSeq, limit: PULL_LIMIT });
177
+ const messages = Array.isArray(result?.messages) ? result.messages : [];
178
+ let maxSeq = Math.max(afterSeq, Number(result?.server_ack_seq) || 0);
179
+ for (const message of messages) {
180
+ const seq = Number(message?.seq);
181
+ if (Number.isSafeInteger(seq) && seq > maxSeq)
182
+ maxSeq = seq;
183
+ }
184
+ if (maxSeq > afterSeq) {
185
+ try {
186
+ await callClient(conn, 'message.ack', { seq: maxSeq });
187
+ }
188
+ catch (error) {
189
+ throw new MenuClientError('ACK_FAILED', error?.message ?? String(error));
190
+ }
191
+ afterSeq = maxSeq;
192
+ }
193
+ if (messages.length < PULL_LIMIT)
194
+ return afterSeq;
195
+ }
196
+ throw new MenuClientError('BASELINE_TOO_LARGE', 'menu-client backlog exceeds the bounded baseline drain');
197
+ }
198
+ async function sendOnce(options, request, timeoutMs) {
199
+ const slotId = options.slotId ?? SLOT.menuClient;
200
+ const encrypt = options.encrypt ?? true;
201
+ if (!encrypt) {
202
+ throw new MenuClientError('INVALID_CLIENT', 'ec-menu-client requires E2EE protected headers; remove --no-encrypt');
203
+ }
204
+ const headers = buildMenuClientProtectedHeaders({
205
+ clientName: 'ec-menu-client',
206
+ clientVersion: readInstalledEvolcoreVersion(),
207
+ remoteAccessToken: options.remoteAccessToken ?? options.ecwebToken,
208
+ });
209
+ const conn = await createShortConnection(options.fromAid, {
210
+ aunPath: options.aunPath,
211
+ slotId,
212
+ shortTtlMs: timeoutMs * (request.type === 'menu.query' || request.type === 'menu.options' ? 2 : 1) + 10_000,
213
+ });
214
+ try {
215
+ let baselineSeq = await drainBaseline(conn);
216
+ const attempts = request.type === 'menu.query' || request.type === 'menu.options' ? 2 : 1;
217
+ let lastError;
218
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
219
+ const sent = await callClient(conn, 'message.send', {
220
+ to: options.targetAid,
221
+ payload: request,
222
+ encrypt,
223
+ ...(encrypt ? { protected_headers: headers } : {}),
224
+ });
225
+ const messageId = sent?.message_id ?? sent?.message?.message_id;
226
+ if (typeof messageId !== 'string' || !messageId.trim()) {
227
+ throw new MenuClientError('SEND_FAILED', 'message.send returned no message_id');
228
+ }
229
+ try {
230
+ return await pollResponse(conn, options.targetAid, request.id, timeoutMs, baselineSeq);
231
+ }
232
+ catch (error) {
233
+ lastError = error;
234
+ if (attempt + 1 < attempts && error instanceof MenuClientError && error.code === 'TIMEOUT') {
235
+ baselineSeq = await drainBaseline(conn);
236
+ continue;
237
+ }
238
+ throw error;
239
+ }
240
+ }
241
+ throw lastError ?? new MenuClientError('INTERNAL_ERROR', 'Menu request failed');
242
+ }
243
+ finally {
244
+ await conn.close();
245
+ }
246
+ }
247
+ export async function executeMenuClientRequest(options) {
248
+ if (!isValidAid(options.fromAid) || !isValidAid(options.targetAid)) {
249
+ throw new MenuClientError('INVALID_ARGUMENT', 'fromAid and targetAid must be valid AIDs');
250
+ }
251
+ if (options.slotId !== undefined && (typeof options.slotId !== 'string' || !options.slotId.trim())) {
252
+ throw new MenuClientError('INVALID_ARGUMENT', 'slotId must be a non-empty string');
253
+ }
254
+ const slotId = options.slotId ?? SLOT.menuClient;
255
+ if (slotId.length > 128 || !/^[A-Za-z0-9._-][A-Za-z0-9._/ :\-]*$/.test(slotId)) {
256
+ throw new MenuClientError('INVALID_ARGUMENT', 'slotId contains unsupported characters');
257
+ }
258
+ if (options.aunPath !== undefined && (typeof options.aunPath !== 'string' || !options.aunPath.trim())) {
259
+ throw new MenuClientError('INVALID_ARGUMENT', 'aunPath must be a non-empty path');
260
+ }
261
+ const request = normalizeRequest(options.request);
262
+ const timeoutMs = normalizeTimeout(options.timeoutMs);
263
+ const release = options.slotId && options.slotId !== SLOT.menuClient
264
+ ? () => { }
265
+ : acquireClientLock(options.aunPath, options.fromAid);
266
+ try {
267
+ return await sendOnce(options, request, timeoutMs);
268
+ }
269
+ finally {
270
+ release();
271
+ }
272
+ }
@@ -297,8 +297,16 @@ export async function groupAck(args) {
297
297
  }
298
298
  }
299
299
  export async function groupCreate(args) {
300
- const store = await getAidStore({ slotId: args.slotId ?? SLOT.cli, aunPath: args.aunPath });
301
- const conn = await loadClient(store, args.from);
300
+ return groupCreateWithDependencies(args, { getAidStore, loadClient });
301
+ }
302
+ /**
303
+ * Injectable boundary for the lifecycle call. Keeping the store/client
304
+ * construction outside the RPC makes the aidStore contract regression-testable
305
+ * without opening a real gateway connection.
306
+ */
307
+ export async function groupCreateWithDependencies(args, dependencies) {
308
+ const store = await dependencies.getAidStore({ slotId: args.slotId ?? SLOT.cli, aunPath: args.aunPath });
309
+ const conn = await dependencies.loadClient(store, args.from);
302
310
  try {
303
311
  await conn.connect({ connection_kind: 'short', short_ttl_ms: 30000, auto_reconnect: false });
304
312
  const params = { name: args.name };
@@ -313,7 +321,7 @@ export async function groupCreate(args) {
313
321
  params.description = args.description;
314
322
  if (args.joinMode)
315
323
  params.join_mode = args.joinMode;
316
- const result = await conn.createGroup(params);
324
+ const result = await conn.createGroup(params, { aidStore: store });
317
325
  return { ok: true, group: result?.group ?? result };
318
326
  }
319
327
  catch (e) {
@@ -510,7 +518,16 @@ export async function groupInvite(args) {
510
518
  try {
511
519
  for (const memberAid of args.members) {
512
520
  try {
513
- await conn.call('group.add_member', { group_id: args.groupId, aid: memberAid });
521
+ // fastaun 0.5.12 introduced targeted invitations. The current gateway
522
+ // supports this lifecycle directly; do not bypass invitee consent with
523
+ // the immediate-membership group.add_member operation.
524
+ const inviteParams = {
525
+ group_id: args.groupId,
526
+ aid: memberAid,
527
+ };
528
+ if (args.memberType !== undefined)
529
+ inviteParams.member_type = args.memberType;
530
+ await conn.call('group.invite_member', inviteParams);
514
531
  added.push(memberAid);
515
532
  }
516
533
  catch (e) {
@@ -523,6 +540,50 @@ export async function groupInvite(args) {
523
540
  await conn.close();
524
541
  }
525
542
  }
543
+ /** List pending targeted invitations for the authenticated AID. */
544
+ export async function groupInvites(args) {
545
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
546
+ try {
547
+ const params = {};
548
+ if (args.page !== undefined)
549
+ params.page = args.page;
550
+ if (args.size !== undefined)
551
+ params.size = args.size;
552
+ const result = await conn.call('group.list_my_invites', params);
553
+ const invites = Array.isArray(result) ? result : Array.isArray(result?.invites)
554
+ ? result.invites
555
+ : Array.isArray(result?.items) ? result.items : [];
556
+ return {
557
+ ok: true,
558
+ invites,
559
+ page: typeof result?.page === 'number' ? result.page : args.page,
560
+ size: typeof result?.size === 'number' ? result.size : args.size,
561
+ nextCursor: typeof result?.next_cursor === 'string' ? result.next_cursor : null,
562
+ };
563
+ }
564
+ catch (e) {
565
+ return formatRpcError(e);
566
+ }
567
+ finally {
568
+ await conn.close();
569
+ }
570
+ }
571
+ async function groupInviteAction(args, method) {
572
+ const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
573
+ try {
574
+ const result = await conn.call(method, { invite_id: args.inviteId });
575
+ return { ok: true, group_id: result?.group_id ?? '', data: result };
576
+ }
577
+ catch (e) {
578
+ return formatRpcError(e);
579
+ }
580
+ finally {
581
+ await conn.close();
582
+ }
583
+ }
584
+ export const groupAcceptInvite = (args) => groupInviteAction(args, 'group.accept_invite');
585
+ export const groupRejectInvite = (args) => groupInviteAction(args, 'group.reject_invite');
586
+ export const groupRevokeInvite = (args) => groupInviteAction(args, 'group.revoke_invite');
526
587
  export async function groupKick(args) {
527
588
  const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
528
589
  try {
@@ -1,4 +1,4 @@
1
1
  export { msgSend, msgPull, msgAck, msgRecall, msgOnline, } from './p2p.js';
2
2
  export { msgHistory, MAX_MSG_HISTORY_LIMIT } from './history.js';
3
- export { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } from './group.js';
3
+ export { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupInvites, groupAcceptInvite, groupRejectInvite, groupRevokeInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } from './group.js';
4
4
  export { uploadFileAndBuildPayload, inferPayloadType, isValidPayloadType } from './upload.js';
@@ -7,7 +7,7 @@ import { isExplicitGroupId } from '../group-identity.js';
7
7
  import { isValidAid } from '../aid/validation.js';
8
8
  import { tryParseChannelKey } from '../../core/channel-loader.js';
9
9
  import { downloadGroupFsBytes } from '../group-fs-download.js';
10
- import { groupBan, groupBanlist, groupCreate, groupDissolve, groupInfo, groupInvite, groupJoin, groupKick, groupLeave, groupList, groupMembers, groupOnline, groupPull, groupAck, groupResume, groupRules, groupRulesFileGet, groupRulesFilePublish, groupRulesFileSet, groupSetRole, groupSuspend, groupTransferOwner, groupUnban, groupUpdate, groupUpdateRules, } from './group.js';
10
+ import { groupBan, groupBanlist, groupCreate, groupDissolve, groupInfo, groupInvite, groupJoin, groupKick, groupLeave, groupList, groupMembers, groupOnline, groupPull, groupAck, groupAcceptInvite, groupInvites, groupRejectInvite, groupRevokeInvite, groupResume, groupRules, groupRulesFileGet, groupRulesFilePublish, groupRulesFileSet, groupSetRole, groupSuspend, groupTransferOwner, groupUnban, groupUpdate, groupUpdateRules, } from './group.js';
11
11
  /** Rules uploaded by a managed task are carried as bounded UTF-8 content. */
12
12
  export const MAX_MANAGED_GROUP_RULES_BYTES = 4 * 1024;
13
13
  const RELATION_GROUP_READ_KINDS = new Set([
@@ -32,7 +32,7 @@ export class ManagedGroupOperationError extends Error {
32
32
  }
33
33
  const GROUP_VALUE_FLAGS = new Set([
34
34
  '--format', '--after-seq', '--limit', '--visibility', '--description', '--join-mode', '--group-id',
35
- '--name', '--page', '--size', '--mode', '--question', '--max-pending', '--message', '--answer', '--duration',
35
+ '--name', '--page', '--size', '--mode', '--question', '--max-pending', '--message', '--answer', '--duration', '--member-type',
36
36
  ]);
37
37
  function failGroup(code, reason) {
38
38
  return { ok: false, code, reason };
@@ -125,6 +125,37 @@ export function resolveManagedGroupOperation(argv) {
125
125
  if (!('positionals' in parsed))
126
126
  return parsed;
127
127
  const { positionals, values } = parsed;
128
+ if (sub === 'invites') {
129
+ const flagsError = onlyFlags(values, ['--page', '--size']);
130
+ if (flagsError)
131
+ return flagsError;
132
+ if (positionals.length !== 1)
133
+ return failGroup('INVALID_ARGUMENT', 'invites accepts exactly <from>');
134
+ const from = requiredAid(positionals[0], 'from AID');
135
+ if (typeof from !== 'string')
136
+ return from;
137
+ const page = numberFlag(values, '--page');
138
+ const size = numberFlag(values, '--size');
139
+ if (typeof page !== 'number' && page !== undefined)
140
+ return page;
141
+ if (typeof size !== 'number' && size !== undefined)
142
+ return size;
143
+ return command(sub, 'ec.group.invites', ['group', sub, from], from, { page, size });
144
+ }
145
+ if (sub === 'accept-invite' || sub === 'reject-invite' || sub === 'revoke-invite') {
146
+ const flagsError = onlyFlags(values, []);
147
+ if (flagsError)
148
+ return flagsError;
149
+ if (positionals.length !== 2)
150
+ return failGroup('INVALID_ARGUMENT', `${sub} accepts exactly <from> <invite-id>`);
151
+ const from = requiredAid(positionals[0], 'from AID');
152
+ if (typeof from !== 'string')
153
+ return from;
154
+ const inviteId = String(positionals[1]).trim();
155
+ if (!inviteId)
156
+ return failGroup('INVALID_ARGUMENT', 'missing invite-id');
157
+ return command(sub, `ec.group.${sub}`, ['group', sub, from, inviteId], from, { inviteId });
158
+ }
128
159
  if (sub === 'list') {
129
160
  const flagsError = onlyFlags(values, ['--size']);
130
161
  if (flagsError)
@@ -170,7 +201,7 @@ export function resolveManagedGroupOperation(argv) {
170
201
  if (simple.has(sub)) {
171
202
  const allowedFlags = {
172
203
  pull: ['--after-seq', '--limit'], ack: [], info: [], update: ['--name', '--description'], dissolve: [],
173
- suspend: [], resume: [], join: ['--message', '--answer'], leave: [], invite: [], kick: [],
204
+ suspend: [], resume: [], join: ['--message', '--answer'], leave: [], invite: ['--member-type'], kick: [],
174
205
  members: ['--page', '--size'], online: [], role: [], owner: [], ban: ['--duration'], unban: [],
175
206
  };
176
207
  const flagsError = onlyFlags(values, allowedFlags[sub] ?? []);
@@ -216,7 +247,11 @@ export function resolveManagedGroupOperation(argv) {
216
247
  if (typeof checked !== 'string')
217
248
  return checked;
218
249
  }
250
+ const memberType = values.get('--member-type');
251
+ if (memberType !== undefined && memberType !== 'human' && memberType !== 'ai')
252
+ return failGroup('INVALID_ARGUMENT', 'invalid --member-type');
219
253
  extra.members = members;
254
+ extra.memberType = memberType;
220
255
  }
221
256
  else if (sub === 'kick' || sub === 'unban') {
222
257
  if (positionals.length !== 3)
@@ -357,7 +392,19 @@ export async function executeManagedGroupOperation(command, rulesContent) {
357
392
  result = await groupLeave({ from: command.from, groupId: command.groupId, ...opts });
358
393
  break;
359
394
  case 'invite':
360
- result = await groupInvite({ from: command.from, groupId: command.groupId, members: command.members, ...opts });
395
+ result = await groupInvite({ from: command.from, groupId: command.groupId, members: command.members, memberType: command.memberType, ...opts });
396
+ break;
397
+ case 'invites':
398
+ result = await groupInvites({ from: command.from, page: command.page, size: command.size, ...opts });
399
+ break;
400
+ case 'accept-invite':
401
+ result = await groupAcceptInvite({ from: command.from, inviteId: command.inviteId, ...opts });
402
+ break;
403
+ case 'reject-invite':
404
+ result = await groupRejectInvite({ from: command.from, inviteId: command.inviteId, ...opts });
405
+ break;
406
+ case 'revoke-invite':
407
+ result = await groupRevokeInvite({ from: command.from, inviteId: command.inviteId, ...opts });
361
408
  break;
362
409
  case 'kick':
363
410
  result = await groupKick({ from: command.from, groupId: command.groupId, memberAid: command.memberAid, ...opts });
@@ -215,6 +215,13 @@ export function findByDedupeKey(aid, dedupeKey, opts = {}, queue = 'default') {
215
215
  : load(aid, queue);
216
216
  return entries.find(entry => entry.dedupeKey === dedupeKey);
217
217
  }
218
+ /** Find all durable entries belonging to an operation family. */
219
+ export function findByDedupePrefix(aid, dedupePrefix, opts = {}, queue = 'default') {
220
+ const entries = opts.includeTerminal
221
+ ? readEntries(aid, queue).filter(entry => !isExpired(entry))
222
+ : load(aid, queue);
223
+ return entries.filter(entry => typeof entry.dedupeKey === 'string' && entry.dedupeKey.startsWith(dedupePrefix));
224
+ }
218
225
  export function cleanup(aid, queue = 'default') {
219
226
  const all = readEntries(aid, queue);
220
227
  let expired = 0;
@@ -3,11 +3,18 @@ export async function createShortConnection(aid, opts) {
3
3
  const store = await getAidStore({ slotId: opts?.slotId ?? SLOT.cli, aunPath: opts?.aunPath });
4
4
  try {
5
5
  const client = await loadClient(store, aid);
6
- await client.connect({ connection_kind: 'short', short_ttl_ms: 30000, auto_reconnect: false });
6
+ await client.connect({ connection_kind: 'short', short_ttl_ms: opts?.shortTtlMs ?? 30000, auto_reconnect: false });
7
7
  return {
8
8
  async call(method, params) {
9
9
  return client.call(method, params);
10
10
  },
11
+ group: {
12
+ inviteMember: (params) => client.group.inviteMember(params),
13
+ listMyInvites: (params) => client.group.listMyInvites(params),
14
+ acceptInvite: (params) => client.group.acceptInvite(params),
15
+ rejectInvite: (params) => client.group.rejectInvite(params),
16
+ revokeInvite: (params) => client.group.revokeInvite(params),
17
+ },
11
18
  async close() {
12
19
  try {
13
20
  await client.close();