evolcore 0.0.35 → 0.0.37

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 (224) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +177 -85
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +115 -36
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -0,0 +1,58 @@
1
+ import { isValidAid } from './validation.js';
2
+ import { ownerResolver, OwnershipError } from '../../core/ownership/owner-resolver.js';
3
+ import { logger } from '../../utils/logger.js';
4
+ export function ownershipFailure(error) {
5
+ const details = error && typeof error === 'object' ? error : {};
6
+ return {
7
+ ok: false,
8
+ code: typeof details.stringCode === 'string' ? details.stringCode
9
+ : typeof details.code === 'string' ? details.code : 'OWNERSHIP_OPERATION_FAILED',
10
+ error: error instanceof Error ? error.message : typeof details.error === 'string' ? details.error : String(error),
11
+ ...(typeof details.retryable === 'boolean' ? { retryable: details.retryable } : {}),
12
+ };
13
+ }
14
+ export function parseOwnershipCommand(argv) {
15
+ const args = argv[0] === 'aid' ? argv.slice(1) : [...argv];
16
+ if (args[0] !== 'bind' || !args[1]) {
17
+ throw new OwnershipError('INVALID_ARGUMENT', 'Use ec aid bind <ownerAid> or ec aid bind status');
18
+ }
19
+ const action = args[1] === 'status' ? 'status' : 'bind';
20
+ if (action === 'bind' && !isValidAid(args[1]))
21
+ throw new OwnershipError('INVALID_AID', 'A valid owner AID is required');
22
+ let subjectAid;
23
+ let formatSeen = false;
24
+ for (let index = 2; index < args.length; index += 2) {
25
+ if (args[index] === '--subject' && !subjectAid && isValidAid(args[index + 1] ?? ''))
26
+ subjectAid = args[index + 1];
27
+ else if (args[index] === '--format' && args[index + 1] === 'json' && !formatSeen)
28
+ formatSeen = true;
29
+ else
30
+ throw new OwnershipError('INVALID_ARGUMENT', 'Only one --subject <subjectAid> and --format json are supported');
31
+ }
32
+ return action === 'status' ? { action, subjectAid } : { action, subjectAid, ownerAid: args[1] };
33
+ }
34
+ export async function executeOwnershipCommand(command, defaultSubjectAid) {
35
+ const subjectAid = command.subjectAid ?? defaultSubjectAid;
36
+ if (!subjectAid)
37
+ throw new OwnershipError('SUBJECT_REQUIRED', 'No local subject AID is available');
38
+ if (command.action === 'status') {
39
+ ownerResolver.assertLocalSubject(subjectAid);
40
+ return ownerResolver.refresh(subjectAid);
41
+ }
42
+ logger.info(`[ownership] bind requested subject=${subjectAid} owner=${command.ownerAid}`);
43
+ try {
44
+ ownerResolver.assertLocalSubject(subjectAid);
45
+ const result = await ownerResolver.bind(subjectAid, command.ownerAid);
46
+ const task = result.task;
47
+ const log = task && ['rejected', 'stopped', 'error'].includes(task.status) ? logger.warn : logger.info;
48
+ log(`[ownership] bind result ${JSON.stringify({ subjectAid, ownerAid: command.ownerAid, operationId: task?.operation_id,
49
+ taskStatus: task?.status, step: task?.step, consensusStatus: result.consensusStatus, requestStatus: result.requestStatus,
50
+ code: task?.code, reason: task?.reason })}`);
51
+ return result;
52
+ }
53
+ catch (error) {
54
+ const details = ownershipFailure(error);
55
+ logger.warn(`[ownership] bind failed subject=${subjectAid} owner=${command.ownerAid} code=${details.code} error=${JSON.stringify(details.error)}`);
56
+ throw error;
57
+ }
58
+ }
@@ -55,6 +55,7 @@ export async function getAidStore(opts) {
55
55
  aunPath,
56
56
  encryptionSeed: AUN_KEYSTORE_ENCRYPTION_SEED,
57
57
  slotId: opts.slotId,
58
+ verifySsl: true,
58
59
  debug: opts.debug ?? false,
59
60
  };
60
61
  if (fs.existsSync(caCertPath))
@@ -1,4 +1,61 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { StorageLowLevel, StorageVFS } from '@agentunion/fastaun';
3
+ export const DEFAULT_FS_CAT_BYTES = 128 * 1024;
4
+ export const MAX_FS_CAT_BYTES = 128 * 1024;
5
+ export class FsReadBudgetError extends Error {
6
+ maxBytes;
7
+ code = 'FS_CAT_TOO_LARGE';
8
+ constructor(maxBytes) {
9
+ super(`cat exceeds read budget ${maxBytes} bytes`);
10
+ this.maxBytes = maxBytes;
11
+ this.name = 'FsReadBudgetError';
12
+ }
13
+ }
14
+ export function validateFsReadBudget(maxBytes) {
15
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_FS_CAT_BYTES) {
16
+ throw new RangeError(`cat read budget must be an integer from 1 to ${MAX_FS_CAT_BYTES}`);
17
+ }
18
+ }
19
+ async function downloadWithinBudget(url, maxBytes, headers) {
20
+ validateFsReadBudget(maxBytes);
21
+ const controller = new AbortController();
22
+ const timeout = setTimeout(() => controller.abort(), 30_000);
23
+ try {
24
+ const response = await fetch(url, { headers, signal: controller.signal });
25
+ if (!response.ok)
26
+ throw new Error(`HTTP GET failed: status=${response.status}`);
27
+ const contentLength = response.headers.get('content-length');
28
+ if (contentLength !== null && Number(contentLength) > maxBytes)
29
+ throw new FsReadBudgetError(maxBytes);
30
+ if (!response.body)
31
+ throw new Error('HTTP GET returned no response body');
32
+ const chunks = [];
33
+ let totalBytes = 0;
34
+ for await (const chunk of response.body) {
35
+ totalBytes += chunk.byteLength;
36
+ if (totalBytes > maxBytes)
37
+ throw new FsReadBudgetError(maxBytes);
38
+ chunks.push(chunk);
39
+ }
40
+ return Buffer.concat(chunks, totalBytes);
41
+ }
42
+ finally {
43
+ clearTimeout(timeout);
44
+ controller.abort();
45
+ }
46
+ }
47
+ export async function readPersonalFsBytesWithinBudget(client, path, maxBytes, options) {
48
+ validateFsReadBudget(maxBytes);
49
+ const lowlevel = new StorageLowLevel(client);
50
+ const getObject = lowlevel.getObject.bind(lowlevel);
51
+ lowlevel.getObject = params => getObject({ ...params, offset: 0, limit: maxBytes + 1 });
52
+ lowlevel.httpGet = (url, headers) => downloadWithinBudget(url, maxBytes, headers);
53
+ const storage = new StorageVFS(client, { lowlevel, useFsRpc: client.storage.useFsRpc });
54
+ const bytes = await storage.readBytes(path, options);
55
+ if (bytes.byteLength > maxBytes)
56
+ throw new FsReadBudgetError(maxBytes);
57
+ return bytes;
58
+ }
2
59
  export class GroupFsDownloadError extends Error {
3
60
  code;
4
61
  constructor(code, message) {
@@ -8,7 +65,9 @@ export class GroupFsDownloadError extends Error {
8
65
  }
9
66
  }
10
67
  /** Download a group file into memory through the ticket/data plane. */
11
- export async function downloadGroupFsBytes(client, path) {
68
+ export async function downloadGroupFsBytes(client, path, options = {}) {
69
+ if (options.maxBytes !== undefined)
70
+ validateFsReadBudget(options.maxBytes);
12
71
  const ticketValue = await client.call('group.fs.create_download_ticket', { path });
13
72
  if (!isRecord(ticketValue)) {
14
73
  throw new GroupFsDownloadError('INVALID_TICKET', 'group.fs.create_download_ticket returned invalid response');
@@ -17,7 +76,13 @@ export async function downloadGroupFsBytes(client, path) {
17
76
  if (!downloadUrl) {
18
77
  throw new GroupFsDownloadError('MISSING_DOWNLOAD_URL', 'group.fs.create_download_ticket did not return download_url');
19
78
  }
20
- const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
79
+ const size = ticketValue.size ?? ticketValue.size_bytes;
80
+ if (options.maxBytes !== undefined && typeof size === 'number' && size > options.maxBytes) {
81
+ throw new FsReadBudgetError(options.maxBytes);
82
+ }
83
+ const bytes = options.maxBytes === undefined
84
+ ? await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client))
85
+ : await downloadWithinBudget(downloadUrl, options.maxBytes, bearerHeaders(client));
21
86
  const expectedSha = stringField(ticketValue, ['sha256']);
22
87
  if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
23
88
  throw new GroupFsDownloadError('HASH_MISMATCH', 'download hash verification failed');
@@ -5,6 +5,8 @@ import { isValidAid } from '../aid/validation.js';
5
5
  import { normalizeGroupAid } from './identity.js';
6
6
  import { resolvePaths } from '../../paths.js';
7
7
  import { chatDirPath } from '../../core/session/session-fs-store.js';
8
+ import { isMainSessionThreadId, normalizeThreadOptionValue } from '../../core/thread/topic-id.js';
9
+ import { parseTimeArg, TIME_ARG_FORMATS } from '../../cli/cli-argv.js';
8
10
  import { isTransientProtocolMessage, messageLogPath, } from '../../core/message/message-log.js';
9
11
  export class GroupHistoryError extends Error {
10
12
  code;
@@ -59,11 +61,19 @@ export function parseGroupHistoryArgs(argv) {
59
61
  const raw = values.get(flag);
60
62
  if (raw === undefined)
61
63
  return undefined;
62
- const result = /^\d+$/.test(raw) ? Number(raw) : Date.parse(raw);
63
- if (!Number.isFinite(result) || result < 0)
64
- fail('INVALID_ARGUMENT', flag + ' 需要有效时间');
64
+ const result = parseTimeArg(raw);
65
+ if (result === undefined)
66
+ fail('INVALID_ARGUMENT', `${flag} 需要有效时间(支持 ${TIME_ARG_FORMATS})`);
65
67
  return result;
66
68
  };
69
+ // `--thread` 接受 `ec thread list` 打印的 `<群AID>/<id>`;前缀必须就是本次查询的群
70
+ let thread = values.get('--thread');
71
+ if (thread !== undefined) {
72
+ const normalized = normalizeThreadOptionValue(thread, peer => normalizeGroupAid(peer, from.slice(from.indexOf('.') + 1)) === groupId);
73
+ if (!normalized.ok)
74
+ return fail('INVALID_ARGUMENT', normalized.error);
75
+ thread = normalized.threadId;
76
+ }
67
77
  const before = time('--before');
68
78
  const after = time('--after');
69
79
  if (before !== undefined && after !== undefined && after >= before)
@@ -81,7 +91,7 @@ export function parseGroupHistoryArgs(argv) {
81
91
  }
82
92
  }
83
93
  return {
84
- from, groupId, thread: values.get('--thread'),
94
+ from, groupId, thread,
85
95
  limit, direction, before, beforeId: values.get('--before-id'),
86
96
  after, afterId: values.get('--after-id'), message: values.get('--message'), offset,
87
97
  };
@@ -170,7 +180,12 @@ function* records(file, args) {
170
180
  }
171
181
  }
172
182
  function matchThread(entry, thread) {
173
- return thread === 'all' || (thread === 'main' ? !entry.threadId : entry.threadId === thread);
183
+ if (thread === 'all')
184
+ return true;
185
+ // 消息日志里主话题有 '' / null / 规范 id 三种历史形态,与 ec msg history 同样归一。
186
+ if (isMainSessionThreadId(thread))
187
+ return isMainSessionThreadId(entry.threadId);
188
+ return entry.threadId === thread;
174
189
  }
175
190
  function utf8Prefix(content, bytes) {
176
191
  const buffer = Buffer.from(content);
@@ -15,6 +15,7 @@ import { resolvePaths } from '../../paths.js';
15
15
  import { readDelegationCredential } from '../../core/auth/agent-delegation.js';
16
16
  import { normalizeAunMentionEntries, withAunAttention } from '../msg/mention-schema.js';
17
17
  import { downloadGroupFsBytes } from './fs-download.js';
18
+ import { isTopicThreadId } from '../../core/thread/topic-id.js';
18
19
  import { normalizeGroupAid, sameGroupAid } from './identity.js';
19
20
  /** Map a group directory probe failure to a stable Menu protocol code. */
20
21
  export function groupDirectoryErrorCode(rawCode, error) {
@@ -53,10 +54,15 @@ function groupThreadId(args, payload, runtimeContext) {
53
54
  : undefined;
54
55
  return payloadThread || args.thread || (isCurrentGroupRuntime(args, runtimeContext) ? runtimeContext?.threadId : undefined);
55
56
  }
56
- function applyGroupRoutingPayloadFields(payload, args, runtimeContext) {
57
+ /** 把群发送的话题路由写进线上 payload。导出供单测固定"主话题 id 不上线"的契约。 */
58
+ export function applyGroupRoutingPayloadFields(payload, args, runtimeContext) {
57
59
  const threadId = groupThreadId(args, payload, runtimeContext);
58
- if (threadId)
60
+ // 主话题的规范 id(`main:<a>:<b>`)是本端身份,不进线上报文:群里的主话题
61
+ // 线上形态就是"不带 thread_id"。`args.thread` 已是规范 id 时同样拦掉。
62
+ if (isTopicThreadId(threadId))
59
63
  payload.thread_id = threadId;
64
+ else if (payload.thread_id !== undefined)
65
+ delete payload.thread_id;
60
66
  }
61
67
  function groupSendLogMetadata(args, payload, runtimeContext) {
62
68
  const classified = payload ? classifyAunPayloadForLog(payload) : undefined;
@@ -7,7 +7,7 @@ import { SLOT } from '../aid/store.js';
7
7
  import { isValidAid } from '../aid/validation.js';
8
8
  import { resolveRoot } from '../../paths.js';
9
9
  import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
10
- import { buildMenuClientProtectedHeaders, parseMenuResponse, validateMenuRequest, } from '../../core/command/menu-protocol.js';
10
+ import { buildMenuClientProtectedHeaders, parseMenuEvent, parseMenuResponse, validateMenuRequest, } from '../../core/command/menu-protocol.js';
11
11
  const DEFAULT_TIMEOUT_MS = 15_000;
12
12
  const MAX_TIMEOUT_MS = 120_000;
13
13
  const POLL_INTERVAL_MS = 200;
@@ -27,11 +27,30 @@ export class MenuClientError extends Error {
27
27
  export class ManagedMenuClient {
28
28
  send;
29
29
  pending = new Map();
30
+ eventHandlers = new Map();
30
31
  constructor(send) {
31
32
  this.send = send;
32
33
  }
33
34
  accept(fromAid, payload, encrypted) {
34
- if (!payload || typeof payload !== 'object' || payload.type !== 'menu.response')
35
+ if (!payload || typeof payload !== 'object')
36
+ return false;
37
+ if (payload.type === 'menu.event') {
38
+ if (!encrypted)
39
+ return true;
40
+ try {
41
+ const event = parseMenuEvent(payload);
42
+ const key = this.key(fromAid, event.requestId);
43
+ const entry = this.eventHandlers.get(key);
44
+ entry?.handler(event);
45
+ if (event.event === 'upgrade.result' && entry) {
46
+ clearTimeout(entry.timer);
47
+ this.eventHandlers.delete(key);
48
+ }
49
+ }
50
+ catch { }
51
+ return true;
52
+ }
53
+ if (payload.type !== 'menu.response')
35
54
  return false;
36
55
  if (!encrypted)
37
56
  return true;
@@ -46,6 +65,9 @@ export class ManagedMenuClient {
46
65
  for (const pending of this.pending.values())
47
66
  pending.reject(new MenuClientError('AUN_DISCONNECTED', 'AUN channel disconnected'));
48
67
  this.pending.clear();
68
+ for (const entry of this.eventHandlers.values())
69
+ clearTimeout(entry.timer);
70
+ this.eventHandlers.clear();
49
71
  }
50
72
  async request(options) {
51
73
  if (!isValidAid(options.fromAid) || !isValidAid(options.targetAid))
@@ -63,6 +85,14 @@ export class ManagedMenuClient {
63
85
  if (this.pending.has(key))
64
86
  throw new MenuClientError('CLIENT_BUSY', 'Request ID is already in flight for this daemon');
65
87
  const headers = buildMenuClientProtectedHeaders({ clientName: 'ec-menu-client', clientVersion: readInstalledEvolcoreVersion(), remoteAccessToken: options.remoteAccessToken ?? options.ecwebToken });
88
+ if (options.onEvent && request.type === 'menu.action' && request.action === 'upgrade' && request.args?.mode === 'update') {
89
+ const existing = this.eventHandlers.get(key);
90
+ if (existing)
91
+ clearTimeout(existing.timer);
92
+ const timer = setTimeout(() => this.eventHandlers.delete(key), 6 * 60_000);
93
+ timer.unref?.();
94
+ this.eventHandlers.set(key, { handler: options.onEvent, timer });
95
+ }
66
96
  const attempts = request.type === 'menu.query' || request.type === 'menu.options' ? 2 : 1;
67
97
  let timer;
68
98
  try {
@@ -82,6 +112,13 @@ export class ManagedMenuClient {
82
112
  });
83
113
  try {
84
114
  const [, value] = await Promise.all([sent, response]);
115
+ if (value.error || !(value.data && typeof value.data === 'object' && typeof value.data.jobId === 'string')) {
116
+ const handler = this.eventHandlers.get(key);
117
+ if (handler) {
118
+ clearTimeout(handler.timer);
119
+ this.eventHandlers.delete(key);
120
+ }
121
+ }
85
122
  return value;
86
123
  }
87
124
  catch (error) {
@@ -30,7 +30,7 @@ export function menuTransportParams(method, params) {
30
30
  if (method !== 'message.send' || typeof params.payload?.type !== 'string'
31
31
  || !params.payload.type.startsWith('menu.'))
32
32
  return params;
33
- if (params.payload.type !== 'menu.response')
33
+ if (params.payload.type !== 'menu.response' && params.payload.type !== 'menu.event')
34
34
  return { ...params, ttl: MENU_MESSAGE_TTL_SECONDS };
35
35
  const { to_device_id: target, menu_client_sdk_version: sdkVersion, menu_client_volatile_v1: capability, ...rest } = params;
36
36
  delete rest.device_id;
@@ -4,6 +4,7 @@ import { isTransientProtocolMessage, messageLogPath } from '../../core/message/m
4
4
  import { chatDirPath, readAllJsonlLines, readJsonFile, readLastJsonlLine, scanChatDirs, scanMetaFiles } from '../../core/session/session-fs-store.js';
5
5
  import { tryParseChannelKey } from '../../core/channel-loader.js';
6
6
  import { resolvePaths } from '../../paths.js';
7
+ import { isMainSessionThreadId } from '../../core/thread/topic-id.js';
7
8
  const DEFAULT_LIMIT = 50;
8
9
  export const MAX_MSG_HISTORY_LIMIT = 500;
9
10
  const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
@@ -52,7 +53,16 @@ function msgHistoryByLegacyRoute(args, options) {
52
53
  return error('INVALID_CHANNEL_NAME', '无效 channel 实例名');
53
54
  }
54
55
  const chatDir = chatDirPath(options.sessionsDir, channel, target, args.from, channel === 'aun' && !args.channelName ? undefined : `${channel}#${args.from}#${args.channelName ?? 'main'}`);
55
- const resolvedSession = resolveLegacySessionFilter(chatDir, args.from, target, channel, args.session ?? 'latest');
56
+ // --thread <话题id> 且未显式指定 --session 时,从 _threads/ 反查对应 sessionId,
57
+ // 避免默认 'latest' 锁定主会话导致话题消息被 sessionId 白名单全部过滤掉。
58
+ let sessionArg = args.session ?? 'latest';
59
+ if (args.threadId !== undefined && !isMainSessionThreadId(args.threadId) && args.session === undefined) {
60
+ const found = findSessionIdByThreadId(chatDir, args.threadId);
61
+ if (!found)
62
+ return error('SESSION_NOT_FOUND', `找不到话题 ${args.threadId} 对应的会话(话题必须存在于本端索引)`);
63
+ sessionArg = found;
64
+ }
65
+ const resolvedSession = resolveLegacySessionFilter(chatDir, args.from, target, channel, sessionArg);
56
66
  if (!resolvedSession.ok)
57
67
  return resolvedSession;
58
68
  return readHistoryScopes({
@@ -165,13 +175,28 @@ function readHistoryScopes(input) {
165
175
  catch (cause) {
166
176
  return error('HISTORY_READ_FAILED', cause instanceof Error ? cause.message : String(cause));
167
177
  }
178
+ // 主话题请求不受会话筛选影响(跨会话总线语义)
179
+ const filteringMainTopic = input.args.threadId !== undefined && isMainSessionThreadId(input.args.threadId);
168
180
  const messages = entries
169
181
  .filter(({ entry }) => isMessageLogEntry(entry))
170
182
  .filter(({ entry }) => !isTransientProtocolMessage(entry))
171
- .filter(({ entry, sessionIds }) => sessionIds === undefined
172
- || (typeof entry.sessionId === 'string' && sessionIds.has(entry.sessionId)))
183
+ // 会话筛选:主话题自动放行,其他按 sessionIds 白名单过滤
184
+ .filter(({ entry, sessionIds }) => {
185
+ if (filteringMainTopic && isMainSessionThreadId(entry.threadId))
186
+ return true;
187
+ return sessionIds === undefined || (typeof entry.sessionId === 'string' && sessionIds.has(entry.sessionId));
188
+ })
173
189
  .filter(({ entry }) => input.direction === 'all' || entry.dir === input.direction)
174
- .filter(({ entry }) => input.args.threadId === undefined || entry.threadId === input.args.threadId)
190
+ // 话题过滤:主话题历史行可能是空值/`main` 或现行规范 id,三者都算同一条主线。
191
+ .filter(({ entry }) => {
192
+ const want = input.args.threadId;
193
+ if (want === undefined)
194
+ return true;
195
+ const actual = entry.threadId;
196
+ if (isMainSessionThreadId(want))
197
+ return isMainSessionThreadId(actual);
198
+ return actual === want;
199
+ })
175
200
  .filter(({ entry }) => input.args.after === undefined || entry.ts > input.args.after)
176
201
  .filter(({ entry }) => input.args.before === undefined || entry.ts < input.args.before)
177
202
  .sort((left, right) => left.entry.ts - right.entry.ts || left.scope - right.scope)
@@ -251,6 +276,20 @@ function nonAunSessionBelongsToPeer(session, selfAid, channelKey, channelType, p
251
276
  return false;
252
277
  return session.metadata?.peerId === peerId;
253
278
  }
279
+ /**
280
+ * 从 `_threads/` 扫描,找出 threadId 字段等于指定话题 id 的 session,返回其 sessionId。
281
+ * 用于 `--thread <话题id>` 未指定 `--session` 时自动定位对应会话,避免回落到主会话。
282
+ */
283
+ function findSessionIdByThreadId(chatDir, threadId) {
284
+ for (const entry of scanMetaFiles(path.join(chatDir, '_threads'))) {
285
+ if (entry.includes('.fork-history'))
286
+ continue;
287
+ const session = readLastJsonlLine(path.join(chatDir, '_threads', entry));
288
+ if (session?.id && session.threadId === threadId)
289
+ return session.id;
290
+ }
291
+ return undefined;
292
+ }
254
293
  /**
255
294
  * `active.json` is only a cache. Session JSONL files are the authoritative
256
295
  * history index, including archived and topic sessions, so selector reads
@@ -7,7 +7,9 @@ import { resolveRoot } from '../../paths.js';
7
7
  import { normalizeGroupAid } from '../group/identity.js';
8
8
  import { isValidAid } from '../aid/validation.js';
9
9
  import { tryParseChannelKey } from '../../core/channel-loader.js';
10
- import { downloadGroupFsBytes } from '../group/fs-download.js';
10
+ import { normalizeThreadOptionValue } from '../../core/thread/topic-id.js';
11
+ import { parseTimeArg, TIME_ARG_FORMATS } from '../../cli/cli-argv.js';
12
+ import { DEFAULT_FS_CAT_BYTES, MAX_FS_CAT_BYTES, FsReadBudgetError, downloadGroupFsBytes, readPersonalFsBytesWithinBudget, } from '../group/fs-download.js';
11
13
  import { groupBan, groupBanlist, groupBlock, groupUnblock, groupBlocklist, groupReportStatus, groupMemberStatuses, 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/operations.js';
12
14
  import { handleGroupMenu, GroupMenuError } from '../../core/command/group-menu.js';
13
15
  import { readRolesConfig } from '../../config/roles.js';
@@ -604,17 +606,6 @@ const HISTORY_VALUE_FLAGS = new Set([
604
606
  function failMsg(code, reason) {
605
607
  return { ok: false, code, reason };
606
608
  }
607
- function parseTime(value) {
608
- const trimmed = value.trim();
609
- if (!trimmed)
610
- return undefined;
611
- if (/^-?\d+$/.test(trimmed)) {
612
- const timestamp = Number(trimmed);
613
- return Number.isSafeInteger(timestamp) ? timestamp : undefined;
614
- }
615
- const timestamp = Date.parse(trimmed);
616
- return Number.isFinite(timestamp) ? timestamp : undefined;
617
- }
618
609
  /** Parse the daemon-owned msg read grammar. AUN keeps positional peer AIDs;
619
610
  * non-AUN history is selected by its canonical channel key and native peer ID. */
620
611
  export function resolveManagedMsgOperation(argv) {
@@ -713,22 +704,28 @@ export function resolveManagedMsgOperation(argv) {
713
704
  }
714
705
  const beforeRaw = values.get('--before');
715
706
  const afterRaw = values.get('--after');
716
- const before = beforeRaw === undefined ? undefined : parseTime(beforeRaw);
717
- const after = afterRaw === undefined ? undefined : parseTime(afterRaw);
707
+ const before = beforeRaw === undefined ? undefined : parseTimeArg(beforeRaw);
708
+ const after = afterRaw === undefined ? undefined : parseTimeArg(afterRaw);
718
709
  if (beforeRaw !== undefined && before === undefined)
719
- return failMsg('INVALID_ARGUMENT', 'invalid --before time');
710
+ return failMsg('INVALID_ARGUMENT', `invalid --before time(支持 ${TIME_ARG_FORMATS})`);
720
711
  if (afterRaw !== undefined && after === undefined)
721
- return failMsg('INVALID_ARGUMENT', 'invalid --after time');
712
+ return failMsg('INVALID_ARGUMENT', `invalid --after time(支持 ${TIME_ARG_FORMATS})`);
722
713
  if (before !== undefined && after !== undefined && after >= before) {
723
714
  return failMsg('INVALID_ARGUMENT', '--after must be earlier than --before');
724
715
  }
725
716
  const target = channelKey === undefined ? String(positionals[0] || '').replace(/^@/, '') : undefined;
726
- const threadId = values.get('--thread');
727
- if (threadId !== undefined) {
717
+ const threadRaw = values.get('--thread');
718
+ let threadId;
719
+ if (threadRaw !== undefined) {
728
720
  if (kind !== 'history')
729
721
  return failMsg('INVALID_ARGUMENT', '--thread is only supported for history');
730
722
  if (channelKey !== undefined)
731
723
  return failMsg('INVALID_ARGUMENT', '--thread cannot be combined with --channel-key/--peer-id');
724
+ // 接受 `ec thread list` 打印的 `<peer>/<id>`;前缀必须就是本次查询的对端
725
+ const normalized = normalizeThreadOptionValue(threadRaw, peer => peer === target);
726
+ if (!normalized.ok)
727
+ return failMsg('INVALID_ARGUMENT', normalized.error);
728
+ threadId = normalized.threadId;
732
729
  if (!/^thread-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(threadId))
733
730
  return failMsg('INVALID_ARGUMENT', 'invalid --thread id');
734
731
  }
@@ -747,8 +744,9 @@ export function resolveManagedMsgOperation(argv) {
747
744
  canonicalArgv.push('--after', values.get('--after'));
748
745
  if (values.has('--direction'))
749
746
  canonicalArgv.push('--direction', values.get('--direction'));
750
- if (threadId !== undefined)
751
- canonicalArgv.push('--thread', threadId);
747
+ // canonicalArgv 进审计 hash,保留调用方原文;归一后的 id 只用于执行
748
+ if (threadRaw !== undefined)
749
+ canonicalArgv.push('--thread', threadRaw);
752
750
  return {
753
751
  ok: true,
754
752
  command: {
@@ -793,7 +791,6 @@ export async function executeManagedMsgOperation(command) {
793
791
  }
794
792
  return result;
795
793
  }
796
- export const MAX_MANAGED_FS_CAT_BYTES = 256 * 1024;
797
794
  const MAX_MANAGED_FS_HEAD_BYTES = 4096;
798
795
  const MAX_REMOTE_PATH_LENGTH = 2048;
799
796
  export class ManagedFsOperationError extends Error {
@@ -816,8 +813,10 @@ function positiveInteger(value, flag, max) {
816
813
  if (value === undefined)
817
814
  return undefined;
818
815
  const number = Number(value);
819
- if (!Number.isInteger(number) || number < 1 || number > max) {
820
- return failFs('INVALID_ARGUMENT', `${flag} must be an integer from 1 to ${max}`);
816
+ if (!Number.isSafeInteger(number) || number < 1 || (max !== undefined && number > max)) {
817
+ return failFs('INVALID_ARGUMENT', max === undefined
818
+ ? `${flag} must be a positive safe integer`
819
+ : `${flag} must be an integer from 1 to ${max}`);
821
820
  }
822
821
  return number;
823
822
  }
@@ -846,7 +845,7 @@ function parseRemote(value, allowEmpty) {
846
845
  }
847
846
  return { targetAid, remotePath };
848
847
  }
849
- /** Parse the daemon-owned managed filesystem grammar for group or self storage. */
848
+ /** Parse the daemon-owned managed filesystem grammar; the daemon resolves the permitted target backend. */
850
849
  export function resolveManagedFsOperation(argv) {
851
850
  const raw = argv[0] === 'fs' ? argv.slice(1) : argv;
852
851
  const rawKind = raw[0];
@@ -1000,7 +999,7 @@ export function resolveManagedFsOperation(argv) {
1000
999
  actorAid, targetAid: remote.targetAid, remotePath: remote.remotePath, recursive, overwrite,
1001
1000
  } };
1002
1001
  const effectiveKind = rawKind === 'stat' && !follow ? 'lstat' : rawKind;
1003
- const maxBytes = positiveInteger(values.get('--max-bytes'), '--max-bytes', MAX_MANAGED_FS_CAT_BYTES);
1002
+ const maxBytes = positiveInteger(values.get('--max-bytes'), '--max-bytes', MAX_FS_CAT_BYTES);
1004
1003
  if (typeof maxBytes !== 'number' && maxBytes !== undefined)
1005
1004
  return maxBytes;
1006
1005
  const headBytes = positiveInteger(values.get('--head-bytes'), '--head-bytes', MAX_MANAGED_FS_HEAD_BYTES);
@@ -1041,14 +1040,6 @@ function parseManagedGroupAclSpec(value, requirePerms) {
1041
1040
  }
1042
1041
  return { grantee, perms };
1043
1042
  }
1044
- function numberFrom(value, keys) {
1045
- for (const key of keys) {
1046
- const number = Number(value?.[key]);
1047
- if (Number.isFinite(number))
1048
- return number;
1049
- }
1050
- return undefined;
1051
- }
1052
1043
  async function withClient(actorAid, fn) {
1053
1044
  const store = await getAidStore({ slotId: SLOT.cli, aunPath: resolveRoot() });
1054
1045
  let client;
@@ -1140,16 +1131,7 @@ export async function executeManagedFsOperationWithClient(command, client) {
1140
1131
  case 'setfacl':
1141
1132
  throw new ManagedFsOperationError('NOT_ALLOWED', 'Managed personal storage ACL changes are not supported');
1142
1133
  case 'cat': {
1143
- const node = await client.storage.stat(command.remotePath, { owner });
1144
- const size = numberFrom(node, ['size', 'sizeBytes', 'size_bytes']);
1145
- const maxBytes = Math.min(command.maxBytes ?? MAX_MANAGED_FS_CAT_BYTES, MAX_MANAGED_FS_CAT_BYTES);
1146
- if (size === undefined)
1147
- throw new ManagedFsOperationError('FS_SIZE_REQUIRED', 'personal file size is required before managed cat');
1148
- if (size > maxBytes)
1149
- throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
1150
- const bytes = await client.storage.readBytes(command.remotePath, { owner });
1151
- if (bytes.byteLength > maxBytes)
1152
- throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
1134
+ const bytes = await readPersonalFsBytesWithinBudget(client, command.remotePath, command.maxBytes ?? DEFAULT_FS_CAT_BYTES, { owner });
1153
1135
  const headBytes = Math.min(command.headBytes ?? 256, MAX_MANAGED_FS_HEAD_BYTES, bytes.byteLength);
1154
1136
  if (bytes.subarray(0, Math.min(bytes.byteLength, 8192)).some(byte => byte === 0)) {
1155
1137
  return {
@@ -1235,16 +1217,9 @@ export async function executeManagedFsOperationWithClient(command, client) {
1235
1217
  return { ok: true, command: 'setfacl', path: ref, grantee: command.aclGrantee, removed: !!command.aclRemove, result };
1236
1218
  }
1237
1219
  case 'cat': {
1238
- const node = await client.group.fs.stat(ref);
1239
- const size = numberFrom(node, ['size', 'size_bytes', 'bytes']);
1240
- const maxBytes = Math.min(command.maxBytes ?? MAX_MANAGED_FS_CAT_BYTES, MAX_MANAGED_FS_CAT_BYTES);
1241
- if (size === undefined)
1242
- throw new ManagedFsOperationError('FS_SIZE_REQUIRED', 'group file size is required before managed cat');
1243
- if (size > maxBytes)
1244
- throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
1245
- const { bytes } = await downloadGroupFsBytes(client, ref);
1246
- if (bytes.byteLength > maxBytes)
1247
- throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
1220
+ const { bytes } = await downloadGroupFsBytes(client, ref, {
1221
+ maxBytes: command.maxBytes ?? DEFAULT_FS_CAT_BYTES,
1222
+ });
1248
1223
  const headBytes = Math.min(command.headBytes ?? 256, MAX_MANAGED_FS_HEAD_BYTES, bytes.byteLength);
1249
1224
  if (bytes.subarray(0, Math.min(bytes.byteLength, 8192)).some(byte => byte === 0)) {
1250
1225
  return {
@@ -1271,6 +1246,8 @@ export async function executeManagedFsOperationWithClient(command, client) {
1271
1246
  catch (error) {
1272
1247
  if (error instanceof ManagedFsOperationError)
1273
1248
  throw error;
1249
+ if (error instanceof FsReadBudgetError)
1250
+ throw new ManagedFsOperationError(error.code, error.message);
1274
1251
  throw new ManagedFsOperationError('FS_OPERATION_FAILED', error instanceof Error ? error.message : String(error));
1275
1252
  }
1276
1253
  }