evolcore 0.0.25 → 0.0.27

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 (304) hide show
  1. package/CHANGELOG.md +632 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +203 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent-availability.js +68 -0
  10. package/dist/agents/baseagent.js +96 -46
  11. package/dist/agents/claude-runner.js +172 -33
  12. package/dist/agents/codex/app-server-client.js +76 -31
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +34 -1
  15. package/dist/agents/codex-runner.js +434 -446
  16. package/dist/agents/ecagent-runner.js +174 -34
  17. package/dist/agents/gemini-runner.js +8 -7
  18. package/dist/agents/runner-types.js +75 -19
  19. package/dist/aun/aid/agentmd.js +151 -36
  20. package/dist/aun/aid/client.js +1 -1
  21. package/dist/aun/aid/control-aid.js +180 -1
  22. package/dist/aun/aid/identity.js +4 -4
  23. package/dist/aun/aid/index.js +1 -1
  24. package/dist/aun/aid/store.js +17 -13
  25. package/dist/aun/group/history.js +303 -0
  26. package/dist/aun/group/identity.js +43 -0
  27. package/dist/aun/group/index.js +5 -0
  28. package/dist/aun/{msg/group.js → group/operations.js} +162 -39
  29. package/dist/aun/{menu-client.js → menu/client.js} +85 -6
  30. package/dist/aun/menu/index.js +2 -0
  31. package/dist/aun/menu/transport.js +49 -0
  32. package/dist/aun/msg/index.js +2 -1
  33. package/dist/aun/msg/managed-operation.js +85 -19
  34. package/dist/aun/msg/mention-schema.js +51 -0
  35. package/dist/aun/msg/p2p.js +19 -9
  36. package/dist/aun/msg/upload.js +43 -80
  37. package/dist/aun/outbox.js +29 -7
  38. package/dist/aun/rpc/client.js +1 -0
  39. package/dist/aun/storage/client.js +39 -50
  40. package/dist/channels/aun.js +842 -318
  41. package/dist/channels/daemon.js +10 -6
  42. package/dist/cli/agent-command.js +212 -89
  43. package/dist/cli/agent-create.js +626 -11
  44. package/dist/cli/agent.js +125 -13
  45. package/dist/cli/aun-commands.js +487 -337
  46. package/dist/cli/bench.js +20 -20
  47. package/dist/cli/config.js +55 -33
  48. package/dist/cli/contact.js +72 -15
  49. package/dist/cli/ctl-command.js +24 -24
  50. package/dist/cli/daemon-commands.js +140 -63
  51. package/dist/cli/diag.js +31 -0
  52. package/dist/cli/fs-command.js +70 -70
  53. package/dist/cli/handoff-command.js +46 -14
  54. package/dist/cli/index.js +91 -79
  55. package/dist/cli/init.js +34 -1
  56. package/dist/cli/link-rules.js +21 -21
  57. package/dist/cli/managed-command-guard.js +5 -1
  58. package/dist/cli/menu-client.js +51 -32
  59. package/dist/cli/model.js +3 -3
  60. package/dist/cli/net-check.js +22 -22
  61. package/dist/cli/queue-command.js +31 -31
  62. package/dist/cli/response.js +32 -32
  63. package/dist/cli/restart-monitor.js +103 -42
  64. package/dist/cli/stats.js +51 -51
  65. package/dist/cli/task-command.js +59 -0
  66. package/dist/cli/task-context.js +97 -49
  67. package/dist/cli/trigger-command.js +74 -64
  68. package/dist/config/builtin-roles.js +11 -0
  69. package/dist/config/config-field-policy.js +11 -0
  70. package/dist/config/config-manager.js +32 -4
  71. package/dist/config/config-operation-service.js +160 -0
  72. package/dist/config/contact-book-store.js +1 -1
  73. package/dist/config/contact-book.js +1 -1
  74. package/dist/config/contact-operation-service.js +97 -8
  75. package/dist/config/peer-role-resolver.js +1 -1
  76. package/dist/config/resolved-config-op.js +30 -7
  77. package/dist/config-store.js +1 -1
  78. package/dist/core/agent-application-service.js +7 -1
  79. package/dist/core/audit/event-key.js +17 -2
  80. package/dist/core/audit/log-integrity.js +34 -8
  81. package/dist/core/auth/agent-delegation.js +48 -14
  82. package/dist/core/auth/auth-gateway.js +2 -0
  83. package/dist/core/auth/authorization-audit.js +53 -49
  84. package/dist/core/auth/operation-authorizer.js +90 -7
  85. package/dist/core/auth/operation-catalog.js +145 -4
  86. package/dist/core/auth/remote-access-store.js +155 -10
  87. package/dist/core/bootstrap-messages.js +66 -7
  88. package/dist/core/bootstrap-service.js +111 -20
  89. package/dist/core/command/agent-control.js +88 -7
  90. package/dist/core/command/agent-storage-menu.js +443 -0
  91. package/dist/core/command/cli-intent-parser.js +19 -0
  92. package/dist/core/command/command-handler.js +159 -65
  93. package/dist/core/command/data-root-resolver.js +273 -0
  94. package/dist/core/command/diag-service.js +118 -0
  95. package/dist/core/command/group-menu.js +7 -2
  96. package/dist/core/command/menu-catalog.js +46 -7
  97. package/dist/core/command/menu-handler.js +392 -100
  98. package/dist/core/command/menu-protocol.js +45 -6
  99. package/dist/core/command/menu-token-store.js +41 -8
  100. package/dist/core/command/role-menu.js +2 -2
  101. package/dist/core/command/slash-gate.js +34 -4
  102. package/dist/core/command/slash-handler.js +123 -43
  103. package/dist/core/event-catalog.js +4 -0
  104. package/dist/core/evolagent-registry.js +28 -2
  105. package/dist/core/group/invite-service.js +924 -0
  106. package/dist/core/handoff/runtime.js +205 -27
  107. package/dist/core/handoff/store.js +193 -4
  108. package/dist/core/interaction-router.js +22 -8
  109. package/dist/core/message/im-renderer.js +146 -34
  110. package/dist/core/message/message-bridge.js +163 -20
  111. package/dist/core/message/message-log.js +5 -3
  112. package/dist/core/message/message-queue.js +99 -41
  113. package/dist/core/message/message-utils.js +1 -0
  114. package/dist/core/message/response-engine.js +755 -144
  115. package/dist/core/model/catalog-service.js +79 -49
  116. package/dist/core/model/context-window-suffix.js +33 -0
  117. package/dist/core/model/model-catalog.js +1 -1
  118. package/dist/core/model/model-permission.js +12 -3
  119. package/dist/core/permission/approval-gateway.js +36 -3
  120. package/dist/core/permission/ec-command-parser.js +112 -41
  121. package/dist/core/permission/protected-paths.js +134 -16
  122. package/dist/core/permission/sandbox-runtime.js +108 -17
  123. package/dist/core/permission/tool-error-code.js +15 -0
  124. package/dist/core/permission/tool-policy.js +9 -8
  125. package/dist/core/relation/peer-identity.js +57 -6
  126. package/dist/core/relation/shared-data.js +106 -0
  127. package/dist/core/role/runtime-policy.js +64 -0
  128. package/dist/core/runtime-lock.js +10 -26
  129. package/dist/core/session/runner-task-history.js +170 -0
  130. package/dist/core/session/session-manager.js +34 -6
  131. package/dist/core/session/session-mapper.js +41 -0
  132. package/dist/core/session/session-turn-coordinator.js +37 -7
  133. package/dist/core/session/session-turns.js +48 -7
  134. package/dist/core/task/task-recovery-service.js +176 -0
  135. package/dist/core/task/task-resume-operation.js +32 -0
  136. package/dist/eck/group-rules-sync.js +5 -3
  137. package/dist/eck/kit-renderer.js +1 -0
  138. package/dist/index.js +588 -217
  139. package/dist/ipc.js +193 -15
  140. package/dist/paths.js +17 -4
  141. package/dist/product.js +2 -0
  142. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  143. package/dist/stats/billing.js +41 -15
  144. package/dist/stats/db.js +159 -159
  145. package/dist/stats/eck-vars.js +9 -9
  146. package/dist/stats/price-resolver.js +28 -10
  147. package/dist/stats/query.js +139 -139
  148. package/dist/stats/role-budget.js +6 -6
  149. package/dist/stats/writer.js +51 -51
  150. package/dist/trigger/event-source.js +19 -0
  151. package/dist/trigger/feedback.js +233 -40
  152. package/dist/trigger/parser.js +55 -5
  153. package/dist/trigger/patch.js +9 -0
  154. package/dist/trigger/scheduler.js +30 -4
  155. package/dist/trigger/validation.js +20 -3
  156. package/dist/utils/cross-platform.js +30 -1
  157. package/dist/utils/ecweb-utils.js +1 -0
  158. package/dist/utils/error-dict.json +160 -160
  159. package/dist/utils/error-utils.js +61 -2
  160. package/dist/utils/evolcore-version.js +129 -0
  161. package/dist/utils/logger.js +26 -14
  162. package/dist/utils/macos-autostart.js +22 -22
  163. package/dist/utils/model-prices.jsonl +23 -23
  164. package/dist/utils/npm-ops.js +176 -35
  165. package/dist/utils/tool-summary.js +59 -3
  166. package/dist/utils/welcome.js +196 -23
  167. package/ecagent/LICENSE +21 -21
  168. package/ecagent/NOTICE +11 -11
  169. package/ecagent/README.md +61 -61
  170. package/ecagent/dist/harness/agent-harness.js +3 -0
  171. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  172. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  173. package/ecagent/dist/harness/messages.js +7 -7
  174. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  175. package/kits/docs/GUIDE.md +20 -20
  176. package/kits/docs/INDEX.md +60 -66
  177. package/kits/docs/aun/CHEATSHEET.md +19 -19
  178. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  179. package/kits/docs/channels/aun.md +69 -56
  180. package/kits/docs/channels/feishu.md +56 -56
  181. package/kits/docs/context-assembly.md +379 -367
  182. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  183. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  184. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  185. package/kits/docs/eck_templates/runtime.template.md +19 -19
  186. package/kits/docs/evolcore/INDEX.md +76 -71
  187. package/kits/docs/evolcore/agent-create.md +202 -165
  188. package/kits/docs/evolcore/agent.md +99 -67
  189. package/kits/docs/evolcore/aid.md +68 -60
  190. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  191. package/kits/docs/evolcore/config.md +241 -195
  192. package/kits/docs/evolcore/contact.md +75 -68
  193. package/kits/docs/evolcore/ctl.md +46 -46
  194. package/kits/docs/evolcore/event.md +216 -216
  195. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  196. package/kits/docs/evolcore/fs.md +110 -110
  197. package/kits/docs/evolcore/group-collaboration.md +244 -244
  198. package/kits/docs/evolcore/group-fs.md +17 -17
  199. package/kits/docs/evolcore/group-rules.md +251 -251
  200. package/kits/docs/evolcore/group.md +267 -152
  201. package/kits/docs/evolcore/menu-client.md +58 -34
  202. package/kits/docs/evolcore/model.md +50 -50
  203. package/kits/docs/evolcore/msg.md +169 -165
  204. package/kits/docs/evolcore/response.md +75 -75
  205. package/kits/docs/evolcore/rpc.md +37 -37
  206. package/kits/docs/evolcore/self-summary.md +29 -29
  207. package/kits/docs/evolcore/stats.md +83 -83
  208. package/kits/docs/evolcore/storage.md +53 -50
  209. package/kits/docs/evolcore/task.md +14 -0
  210. package/kits/docs/evolcore/trigger.md +623 -575
  211. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  212. package/kits/docs/identity/PATH_OPS.md +16 -16
  213. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  214. package/kits/docs/identity/identity-tools.md +26 -26
  215. package/kits/docs/path-registry.md +62 -62
  216. package/kits/docs/prompt-loading-architecture.md +269 -269
  217. package/kits/docs/venues/aun-group.md +45 -45
  218. package/kits/docs/venues/aun-private.md +10 -10
  219. package/kits/docs/venues/client-desktop.md +10 -10
  220. package/kits/docs/venues/client-mobile.md +10 -10
  221. package/kits/docs/venues/feishu-group.md +13 -13
  222. package/kits/docs/venues/feishu-private.md +9 -9
  223. package/kits/docs/venues/group.md +42 -25
  224. package/kits/docs/venues/private.md +10 -10
  225. package/kits/eck_manifest.auxiliary.json +43 -43
  226. package/kits/eck_manifest.json +217 -190
  227. package/kits/eck_message_manifest.json +74 -74
  228. package/kits/migrations/README-role-config-v4.md +32 -32
  229. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  230. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  231. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  232. package/kits/migrations/rename-config-file.mjs +99 -99
  233. package/kits/rules/01-overview.md +166 -162
  234. package/kits/rules/02-navigation.md +96 -96
  235. package/kits/rules/03-identity.md +36 -36
  236. package/kits/rules/04-relation.md +76 -75
  237. package/kits/rules/05-venue.md +60 -60
  238. package/kits/rules/06-channel.md +65 -63
  239. package/kits/schemas/_meta.json +53 -53
  240. package/kits/schemas/agent-config.schema.1.json +177 -177
  241. package/kits/schemas/agent-config.schema.10.json +395 -395
  242. package/kits/schemas/agent-config.schema.11.json +421 -421
  243. package/kits/schemas/agent-config.schema.12.json +451 -427
  244. package/kits/schemas/agent-config.schema.2.json +239 -239
  245. package/kits/schemas/agent-config.schema.3.json +119 -119
  246. package/kits/schemas/agent-config.schema.4.json +208 -208
  247. package/kits/schemas/agent-config.schema.5.json +326 -326
  248. package/kits/schemas/agent-config.schema.6.json +322 -322
  249. package/kits/schemas/agent-config.schema.7.json +303 -303
  250. package/kits/schemas/agent-config.schema.8.json +304 -304
  251. package/kits/schemas/agent-config.schema.9.json +364 -364
  252. package/kits/schemas/contact-book.schema.1.json +36 -36
  253. package/kits/schemas/contact-book.schema.2.json +43 -43
  254. package/kits/schemas/contact-book.schema.3.json +67 -67
  255. package/kits/schemas/daemon.schema.1.json +91 -91
  256. package/kits/schemas/daemon.schema.2.json +101 -101
  257. package/kits/schemas/daemon.schema.3.json +123 -123
  258. package/kits/schemas/daemon.schema.4.json +136 -136
  259. package/kits/schemas/daemon.schema.5.json +135 -135
  260. package/kits/schemas/daemon.schema.6.json +131 -131
  261. package/kits/schemas/defaults.schema.1.json +81 -81
  262. package/kits/schemas/defaults.schema.2.json +85 -85
  263. package/kits/schemas/defaults.schema.3.json +73 -73
  264. package/kits/schemas/defaults.schema.4.json +107 -107
  265. package/kits/schemas/defaults.schema.5.json +119 -119
  266. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  267. package/kits/schemas/migrations/README.md +30 -30
  268. package/kits/schemas/relation-config.schema.1.json +158 -158
  269. package/kits/schemas/relation-config.schema.2.json +73 -73
  270. package/kits/schemas/relation-config.schema.3.json +50 -50
  271. package/kits/schemas/relation-config.schema.4.json +47 -47
  272. package/kits/schemas/relation-config.schema.5.json +47 -47
  273. package/kits/schemas/relation-config.schema.6.json +46 -46
  274. package/kits/schemas/relation-config.schema.7.json +59 -59
  275. package/kits/schemas/relation-config.schema.8.json +103 -103
  276. package/kits/schemas/role-config.schema.1.json +215 -201
  277. package/kits/schemas/role-registry.schema.1.json +35 -35
  278. package/kits/schemas/single-session.schema.1.json +31 -31
  279. package/kits/schemas/single-session.schema.2.json +57 -57
  280. package/kits/schemas/single-session.schema.3.json +32 -32
  281. package/kits/templates/bootstrap-welcome.md +17 -17
  282. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  283. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  284. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  285. package/kits/templates/message-fragments/inject-default.md +2 -2
  286. package/kits/templates/message-fragments/item.md +2 -2
  287. package/kits/templates/roles/admin.json +92 -92
  288. package/kits/templates/roles/member.json +101 -96
  289. package/kits/templates/roles/owner.json +9 -9
  290. package/kits/templates/roles/visitor.json +90 -85
  291. package/kits/templates/system-fragments/baseagent.md +14 -14
  292. package/kits/templates/system-fragments/bootstrap.md +53 -34
  293. package/kits/templates/system-fragments/channel.md +28 -64
  294. package/kits/templates/system-fragments/commands.md +36 -28
  295. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  296. package/kits/templates/system-fragments/identity.md +13 -13
  297. package/kits/templates/system-fragments/relation.md +19 -19
  298. package/kits/templates/system-fragments/reply.md +56 -0
  299. package/kits/templates/system-fragments/session.md +59 -59
  300. package/kits/templates/system-fragments/venue.md +31 -31
  301. package/package.json +104 -98
  302. package/dist/aun/group-identity.js +0 -9
  303. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  304. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -5,6 +5,10 @@ import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, list
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
7
  import { resolvePaths, agentDir, agentRelationConfig } from '../paths.js';
8
+ import { roleMenuQuery, roleMenuUpdate } from '../core/command/role-menu.js';
9
+ import { parsePeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
10
+ import { isExplicitGroupId } from '../aun/group/index.js';
11
+ import { isUnsupportedBaseagentConfigField } from './config-field-policy.js';
8
12
  export const AUTHENTICATED_CONFIG_ROLE_ENV = 'EVOLCORE_AUTHENTICATED_CONFIG_ROLE';
9
13
  export function executeResolvedConfigCommand(command, options = {}) {
10
14
  try {
@@ -23,11 +27,150 @@ export function executeResolvedConfigCommand(command, options = {}) {
23
27
  return { ok: false, code: 'CONFIG_ERROR', error: error instanceof Error ? error.message : String(error) };
24
28
  }
25
29
  }
30
+ /**
31
+ * Execute a config command that may use an asynchronous domain service.
32
+ * Relation role assignments are intentionally handled here instead of by
33
+ * mutateConfig(), so Role Menu remains the single write path for validation,
34
+ * revisions, journaling, and audit.
35
+ */
36
+ export async function executeResolvedConfigCommandAsync(command, options = {}) {
37
+ try {
38
+ if (isRelationRoleCommand(command)) {
39
+ return await executeRelationRoleCommand(command, options);
40
+ }
41
+ return executeResolvedConfigCommand(command, options);
42
+ }
43
+ catch (error) {
44
+ if (error instanceof ConfigError)
45
+ return { ok: false, code: error.code, error: error.message, ...(error.data !== undefined ? { data: error.data } : {}) };
46
+ const roleError = error;
47
+ return {
48
+ ok: false,
49
+ code: roleError?.code || 'CONFIG_ERROR',
50
+ error: roleError?.message || String(error),
51
+ ...(roleError?.data !== undefined ? { data: roleError.data } : {}),
52
+ };
53
+ }
54
+ }
55
+ function isRelationRoleCommand(command) {
56
+ return command.kind === 'field'
57
+ && command.field === 'role'
58
+ && command.configScope === 'relation'
59
+ && (command.operationId === 'role.relation.read'
60
+ || command.operationId === 'role.assign'
61
+ || command.operationId === 'role.revoke');
62
+ }
63
+ async function executeRelationRoleCommand(op, options) {
64
+ if (!op.self || !op.peerKey) {
65
+ return { ok: false, code: 'PEER_REQUIRED', error: 'role requires --self <aid> --peer <peer-aid>' };
66
+ }
67
+ const target = roleTargetFromPeerKey(op.self, op.peerKey);
68
+ const context = options.roleContext ?? {
69
+ self: op.self,
70
+ actorRole: options.role || 'owner',
71
+ actorRoleSource: 'local-cli',
72
+ isDaemonOwner: false,
73
+ fromControlChannel: false,
74
+ aunAvailable: true,
75
+ };
76
+ if (context.self !== op.self) {
77
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Role target does not match the receiving Agent' };
78
+ }
79
+ const targetArgs = { self: op.self, view: 'target', target };
80
+ const current = await roleMenuQuery(context, targetArgs);
81
+ const explicitRole = current?.assignment?.explicitRole
82
+ ?? current?.assignment?.explicitDefaultRole
83
+ ?? null;
84
+ const effectiveRole = current?.effectiveRole?.roleId
85
+ ?? current?.baselineRole?.roleId
86
+ ?? null;
87
+ const effectiveRoleSource = current?.effectiveRole?.source
88
+ ?? current?.baselineRole?.source
89
+ ?? 'none';
90
+ const roleDetails = { explicitRole, effectiveRole, effectiveRoleSource };
91
+ if (op.subcommand === 'get') {
92
+ return {
93
+ ok: true,
94
+ subcommand: 'get',
95
+ field: op.field,
96
+ value: explicitRole,
97
+ scope: op.configScope,
98
+ roleDetails,
99
+ };
100
+ }
101
+ const nextValue = op.subcommand === 'unset' ? null : op.value;
102
+ const updated = await roleMenuUpdate(context, {
103
+ self: op.self,
104
+ resource: 'assignment',
105
+ target,
106
+ expectedPolicyRevision: current.policyRevision,
107
+ expectedRelationRevision: current.relationRevision,
108
+ }, JSON.stringify(nextValue));
109
+ const updatedExplicitRole = updated?.assignment?.explicitRole
110
+ ?? updated?.assignment?.explicitDefaultRole
111
+ ?? null;
112
+ const updatedEffectiveRole = updated?.effectiveRole?.roleId
113
+ ?? updated?.baselineRole?.roleId
114
+ ?? null;
115
+ const updatedEffectiveRoleSource = updated?.effectiveRole?.source
116
+ ?? updated?.baselineRole?.source
117
+ ?? 'none';
118
+ const updatedDetails = {
119
+ explicitRole: updatedExplicitRole,
120
+ effectiveRole: updatedEffectiveRole,
121
+ effectiveRoleSource: updatedEffectiveRoleSource,
122
+ };
123
+ if (op.subcommand === 'unset') {
124
+ return {
125
+ ok: true,
126
+ subcommand: 'unset',
127
+ field: op.field,
128
+ removed: updated.changed === true,
129
+ scope: op.configScope,
130
+ roleDetails: updatedDetails,
131
+ };
132
+ }
133
+ return {
134
+ ok: true,
135
+ subcommand: 'set',
136
+ field: op.field,
137
+ value: updatedExplicitRole,
138
+ scope: op.configScope,
139
+ file: 'relation-config',
140
+ roleDetails: updatedDetails,
141
+ };
142
+ }
143
+ function roleTargetFromPeerKey(self, peerKey) {
144
+ let parsed;
145
+ try {
146
+ parsed = parsePeerKey(peerKey);
147
+ }
148
+ catch {
149
+ throw new ConfigError('INVALID_SELECTOR', `Invalid relation peer key: ${peerKey}`);
150
+ }
151
+ if (parsed.channelType !== 'aun') {
152
+ throw new ConfigError('NOT_SUPPORTED', 'Relation role assignments currently support AUN peers only');
153
+ }
154
+ const relation = read(ConfigTarget.Relation, { self, peerKey });
155
+ const identity = PeerIdentityCache.get('aun', parsed.channelId, agentDir(self));
156
+ const verifiedType = identity?.aid === parsed.channelId && identity.source === 'agentmd' && identity.verifiedAt > 0
157
+ ? identity.type : undefined;
158
+ const isGroup = relation?.targetKind ? relation.targetKind === 'group'
159
+ : verifiedType ? verifiedType === 'group' : isExplicitGroupId(parsed.channelId);
160
+ return isGroup
161
+ ? { kind: 'group', groupId: parsed.channelId }
162
+ : { kind: 'private', peerAid: parsed.channelId };
163
+ }
26
164
  export function executeResolvedConfigOperation(op, options = {}) {
27
165
  try {
28
166
  const missingAgent = selectedAgentError(op);
29
167
  if (missingAgent)
30
168
  return missingAgent;
169
+ if (op.configScope === 'agent'
170
+ && (op.field === 'fullaccess' || op.field.startsWith('fullaccess.'))
171
+ && op.subcommand !== 'get') {
172
+ return { ok: false, code: 'FORBIDDEN', error: 'Agent fullaccess can only be configured in ECWeb Agent settings' };
173
+ }
31
174
  if (op.subcommand === 'get')
32
175
  return executeGet(op, options);
33
176
  if (op.subcommand === 'set')
@@ -222,6 +365,9 @@ function executeSchema(command) {
222
365
  };
223
366
  }
224
367
  function executeGet(op, options) {
368
+ if (isUnsupportedBaseagentConfigField(op.field)) {
369
+ return { ok: false, code: 'UNSUPPORTED_FIELD', error: `Config field ${op.field} is not supported by this version` };
370
+ }
225
371
  const selector = selectorFor(op, options);
226
372
  if (op.configScope === 'process') {
227
373
  const config = read(ConfigTarget.Process, selector) || {};
@@ -298,6 +444,13 @@ function getWithSchemaDefault(op, absent, note) {
298
444
  return { ok: true, subcommand: 'get', field: op.field, value: absent, scope: op.configScope, ...nt };
299
445
  }
300
446
  function executeSet(op, options) {
447
+ if (op.field === 'role') {
448
+ return {
449
+ ok: false,
450
+ code: 'ROLE_POLICY_MANAGED',
451
+ error: 'Relation role assignments require the asynchronous role assignment service',
452
+ };
453
+ }
301
454
  if (!op.route)
302
455
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
303
456
  const selector = selectorFor(op, options);
@@ -341,6 +494,13 @@ function executeSet(op, options) {
341
494
  };
342
495
  }
343
496
  function executeUnset(op, options) {
497
+ if (op.field === 'role') {
498
+ return {
499
+ ok: false,
500
+ code: 'ROLE_POLICY_MANAGED',
501
+ error: 'Relation role assignments require the asynchronous role assignment service',
502
+ };
503
+ }
344
504
  if (!op.route)
345
505
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
346
506
  const selector = selectorFor(op, options);
@@ -2,7 +2,7 @@ import crypto from 'crypto';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { isValidAid } from '../aun/aid/validation.js';
5
- import { isExplicitGroupId } from '../aun/group-identity.js';
5
+ import { isExplicitGroupId } from '../aun/group/index.js';
6
6
  import { agentConfig, agentContactConfig, agentDir, resolvePaths } from '../paths.js';
7
7
  import { atomicReadJson, atomicWrite, atomicWriteJson } from '../utils/atomic-write.js';
8
8
  import { fileCache } from '../core/daemon-file-cache.js';
@@ -1,6 +1,6 @@
1
1
  import { agentContactConfig } from '../paths.js';
2
2
  import { isValidAid } from '../aun/aid/validation.js';
3
- import { isExplicitGroupId } from '../aun/group-identity.js';
3
+ import { isExplicitGroupId } from '../aun/group/index.js';
4
4
  import { ConfigTarget, read, validateConfig } from './config-manager.js';
5
5
  import { ContactMutationError, contactRevision, mutateContactBookSync } from './contact-book-store.js';
6
6
  import { formatContactAlias, normalizeContactChannelKey, parseContactAlias, } from './contact-alias.js';
@@ -1,10 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import { isValidAid } from '../aun/aid/validation.js';
3
- import { isExplicitGroupId } from '../aun/group-identity.js';
3
+ import { isExplicitGroupId } from '../aun/group/index.js';
4
4
  import { agentConfig } from '../paths.js';
5
5
  import { getContactSnapshot, resolveContactView } from './contact-book.js';
6
6
  import { ContactMutationError } from './contact-book-store.js';
7
7
  import { addContact, mutateContactWithOperation } from './contact-request-service.js';
8
+ import { expirePendingContactRequests, reviewContactRequest } from './contact-request-service.js';
9
+ import { listStaticAgentOwners } from './peer-role-resolver.js';
8
10
  export function resolveContactCommand(argv) {
9
11
  const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
10
12
  const selfAid = String(argValue(args, '--self') || '').trim();
@@ -15,12 +17,41 @@ export function resolveContactCommand(argv) {
15
17
  if (extra.length > 0)
16
18
  return invalid('INVALID_ARGUMENT', `Unexpected contact arguments: ${extra.join(' ')}`);
17
19
  if (command === 'list') {
18
- if (subcommand !== undefined || !args.includes('--blocked')) {
19
- return invalid('INVALID_ARGUMENT', 'Only ec contact list --blocked is currently supported');
20
+ if (subcommand !== undefined) {
21
+ return invalid('INVALID_ARGUMENT', 'contact list does not accept a positional argument');
20
22
  }
23
+ const filter = args.includes('--blocked')
24
+ ? 'blocked'
25
+ : args.includes('--pending')
26
+ ? 'pending'
27
+ : args.includes('--all') ? 'all' : undefined;
28
+ if (!filter)
29
+ return invalid('INVALID_ARGUMENT', 'contact list requires --blocked, --pending, or --all');
21
30
  return {
22
31
  ok: true,
23
- command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, canonicalArgv: canonicalArgv(argv) },
32
+ command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, filter, canonicalArgv: canonicalArgv(argv) },
33
+ };
34
+ }
35
+ if (command === 'approve' || command === 'reject' || command === 'block-request') {
36
+ if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
37
+ return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
38
+ }
39
+ const requestId = String(argValue(args, '--request-id') || '').trim();
40
+ if (!requestId)
41
+ return invalid('INVALID_ARGUMENT', `${command} requires --request-id <request-id>`);
42
+ const expectedContactRevision = argValue(args, '--contact-revision')?.trim() || undefined;
43
+ return {
44
+ ok: true,
45
+ command: {
46
+ kind: 'review-request',
47
+ operationId: 'contact.review',
48
+ selfAid,
49
+ primaryId: subcommand,
50
+ requestId,
51
+ decision: command === 'approve' ? 'approve' : command === 'reject' ? 'reject' : 'block',
52
+ expectedContactRevision,
53
+ canonicalArgv: canonicalArgv(argv),
54
+ },
24
55
  };
25
56
  }
26
57
  if (command === 'block' || command === 'unblock') {
@@ -62,13 +93,63 @@ export async function executeResolvedContactCommand(command, actor = 'local-cli'
62
93
  }
63
94
  try {
64
95
  if (command.kind === 'list-blocked') {
96
+ await expirePendingContactRequests(command.selfAid);
65
97
  const snapshot = getContactSnapshot(command.selfAid);
98
+ const ids = command.filter === 'blocked'
99
+ ? [...snapshot.blockedIndex]
100
+ : [...snapshot.contacts.entries()]
101
+ .filter(([, entry]) => command.filter === 'all' || entry.status === 'pending')
102
+ .map(([id]) => id);
103
+ const items = ids.sort().map(id => contactItem(command.selfAid, id));
104
+ if (command.filter === 'blocked') {
105
+ return {
106
+ ok: true,
107
+ operation: 'list-blocked',
108
+ selfAid: command.selfAid,
109
+ contactRevision: snapshot.contactRevision,
110
+ items,
111
+ };
112
+ }
66
113
  return {
67
114
  ok: true,
68
- operation: 'list-blocked',
115
+ operation: 'list-contacts',
69
116
  selfAid: command.selfAid,
117
+ filter: command.filter,
70
118
  contactRevision: snapshot.contactRevision,
71
- items: [...snapshot.blockedIndex].sort().map(id => resolveContactView(command.selfAid, id)),
119
+ items,
120
+ };
121
+ }
122
+ if (command.kind === 'review-request') {
123
+ const expectedContactRevision = command.expectedContactRevision ?? getContactSnapshot(command.selfAid).contactRevision;
124
+ const reviewerAid = actor === 'local-cli'
125
+ ? listStaticAgentOwners(command.selfAid)[0]
126
+ : actor;
127
+ if (!reviewerAid) {
128
+ return { ok: false, code: 'NO_OWNER', error: `Agent has no owner: ${command.selfAid}` };
129
+ }
130
+ const reviewed = await reviewContactRequest({
131
+ selfAid: command.selfAid,
132
+ primaryId: command.primaryId,
133
+ requestId: command.requestId,
134
+ expectedContactRevision,
135
+ approverId: reviewerAid,
136
+ actorId: reviewerAid,
137
+ decision: command.decision,
138
+ });
139
+ if (reviewed.code !== 'approved' && reviewed.code !== 'rejected' && reviewed.code !== 'blocked') {
140
+ const code = reviewed.code === 'request-expired' ? 'REQUEST_EXPIRED'
141
+ : reviewed.code === 'revision-conflict' ? 'CONFLICT'
142
+ : reviewed.code === 'forbidden' ? 'PERMISSION_DENIED' : 'REQUEST_STALE';
143
+ return { ok: false, code, error: reviewed.code, data: { contactRevision: reviewed.contactRevision } };
144
+ }
145
+ return {
146
+ ok: true,
147
+ operation: 'review-request',
148
+ selfAid: command.selfAid,
149
+ primaryId: command.primaryId,
150
+ requestId: command.requestId,
151
+ result: reviewed.code,
152
+ contactRevision: reviewed.contactRevision,
72
153
  };
73
154
  }
74
155
  if (command.kind === 'set-blocked') {
@@ -129,16 +210,24 @@ function stripOptions(args) {
129
210
  const result = [];
130
211
  for (let index = 0; index < args.length; index += 1) {
131
212
  const arg = args[index];
132
- if (arg === '--self' || arg === '--format') {
213
+ if (arg === '--self' || arg === '--format' || arg === '--request-id' || arg === '--contact-revision') {
133
214
  index += 1;
134
215
  continue;
135
216
  }
136
- if (arg === '--blocked')
217
+ if (arg === '--blocked' || arg === '--pending' || arg === '--all')
137
218
  continue;
138
219
  result.push(arg);
139
220
  }
140
221
  return result;
141
222
  }
223
+ function contactItem(selfAid, primaryId) {
224
+ const view = resolveContactView(selfAid, primaryId);
225
+ const entry = getContactSnapshot(selfAid).contacts.get(primaryId);
226
+ return {
227
+ ...view,
228
+ ...(entry?.pendingRequest ? { pendingRequest: { ...entry.pendingRequest } } : {}),
229
+ };
230
+ }
142
231
  function canonicalArgv(argv) {
143
232
  return argv[0] === 'contact' ? [...argv] : ['contact', ...argv];
144
233
  }
@@ -10,7 +10,7 @@ import { getRoleDefinition, getRoleRank, normalizeDefaultRole, roleExists } from
10
10
  import { getStoredRoleRegistry, roleRegistryExists } from './role-store.js';
11
11
  import { resolvePrincipal } from './contact-book.js';
12
12
  import { isValidAid } from '../aun/aid/validation.js';
13
- import { isExplicitGroupId } from '../aun/group-identity.js';
13
+ import { isExplicitGroupId } from '../aun/group/index.js';
14
14
  export function isAuthenticated(userId) {
15
15
  return isValidAid(String(userId || '').trim());
16
16
  }
@@ -97,11 +97,15 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
97
97
  reason: 'permissionMode is defined by roles; use the role policy editor',
98
98
  };
99
99
  }
100
- if (field === 'role') {
100
+ if (subcommand !== 'get' && (field === 'initialization' || field.startsWith('initialization.'))) {
101
+ return { ok: false, code: 'INITIALIZATION_MANAGED', reason: 'initialization is owned by the creation service' };
102
+ }
103
+ const isRelationRole = field === 'role';
104
+ if (isRelationRole && selector.scope !== 'relation') {
101
105
  return {
102
106
  ok: false,
103
- code: 'ROLE_POLICY_MANAGED',
104
- reason: 'Relation role assignments are managed through the role assignment service',
107
+ code: 'RELATION_SELECTOR_REQUIRED',
108
+ reason: `config ${subcommand} role needs a relation target: --self <aid> --peer <peer-aid>`,
105
109
  };
106
110
  }
107
111
  if (subcommand !== 'get' && (field === 'roles' || field.startsWith('roles.'))) {
@@ -129,12 +133,14 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
129
133
  reason: `Config field ${field} does not support object writes`,
130
134
  };
131
135
  }
132
- const commandScope = selector.scope === 'defaults' ? 'process' : selector.scope;
136
+ const commandScope = isRelationRole ? 'agent' : selector.scope === 'defaults' ? 'process' : selector.scope;
133
137
  const isSafe = fieldRule.class === 'safe-scalar' || fieldRule.class === 'safe-readonly-object';
134
138
  const isManagementOperation = commandScope === 'process' || !isSafe;
135
- const operationId = isManagementOperation
136
- ? subcommand === 'get' ? 'config.read' : 'config.write'
137
- : `config.${subcommand}`;
139
+ const operationId = isRelationRole
140
+ ? subcommand === 'get' ? 'role.relation.read' : subcommand === 'set' ? 'role.assign' : 'role.revoke'
141
+ : isManagementOperation
142
+ ? subcommand === 'get' ? 'config.read' : 'config.write'
143
+ : `config.${subcommand}`;
138
144
  let route;
139
145
  const routeRequired = subcommand !== 'get' || !isManagementOperation;
140
146
  try {
@@ -173,6 +179,23 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
173
179
  return parsedOwners;
174
180
  value = parsedOwners.value;
175
181
  }
182
+ else if (isRelationRole) {
183
+ const raw = rawValue.trim();
184
+ if (!raw)
185
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'Config field role cannot be empty' };
186
+ try {
187
+ const parsed = JSON.parse(raw);
188
+ if (typeof parsed === 'string')
189
+ value = parsed;
190
+ else if (parsed === null)
191
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'config set role requires a role ID; use config unset role to revoke it' };
192
+ else
193
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'Config field role must be a role ID or null' };
194
+ }
195
+ catch {
196
+ value = raw;
197
+ }
198
+ }
176
199
  else if (fieldRule.class === 'safe-scalar') {
177
200
  const parsedValue = parseConfigFieldValue(field, rawValue);
178
201
  if (!parsedValue.ok)
@@ -58,7 +58,7 @@ export function loadDaemonConfig() {
58
58
  return validateDaemonConfig(normalizeProcessConfigCompat(normalizeDaemonServices(sanitized)), false);
59
59
  }
60
60
  export function isFullAccessEnabled(config = loadDaemonConfig()) {
61
- return config.fullaccess?.enabled === true;
61
+ return config.fullaccess?.enabled === undefined || config.fullaccess.enabled === true;
62
62
  }
63
63
  let eckSnapshotsConfigCache;
64
64
  /** Parse the process-level snapshot gate once for the current daemon lifecycle. */
@@ -48,6 +48,8 @@ function errorFrom(error, fallback = 'agent operation failed') {
48
48
  */
49
49
  export class AgentApplicationService {
50
50
  deps;
51
+ initialization;
52
+ setInitializationService(service) { this.initialization = service; }
51
53
  constructor(deps) {
52
54
  this.deps = deps;
53
55
  }
@@ -85,13 +87,14 @@ export class AgentApplicationService {
85
87
  unique_peer_count: stats?.uniquePeerCount ?? 0,
86
88
  }
87
89
  : null;
88
- const config = agent.config;
90
+ const config = (loadAgent(aid) ?? agent.config);
89
91
  const identity = readAgentMdIdentity(aid);
90
92
  const paths = resolvePaths();
91
93
  return {
92
94
  ok: true,
93
95
  aid,
94
96
  status: info.status || 'stopped',
97
+ lifecycle: config?.lifecycle ?? null,
95
98
  identity,
96
99
  config: {
97
100
  baseagent: info.baseagent || null,
@@ -211,6 +214,8 @@ export class AgentApplicationService {
211
214
  }
212
215
  if (!config)
213
216
  return { ok: false, error: `Agent "${aid}" not found`, code: 'NOT_FOUND' };
217
+ if (!enabled)
218
+ await this.initialization?.cancel(aid);
214
219
  const previousEnabled = config.enabled;
215
220
  try {
216
221
  config.enabled = enabled;
@@ -239,6 +244,7 @@ export class AgentApplicationService {
239
244
  return { ok: true, aid, enabled, reloaded: result.ok };
240
245
  }
241
246
  async delete(aid, purge = false) {
247
+ await this.initialization?.cancel(aid);
242
248
  const paths = resolvePaths();
243
249
  const agentDir = path.join(paths.agentsDir, aid);
244
250
  const configPath = path.join(agentDir, 'config.json');
@@ -7,16 +7,31 @@ function stableDigest(parts) {
7
7
  const material = parts.map(part => compactPart(part) ?? '-').join('\u0000');
8
8
  return crypto.createHash('sha256').update(material).digest('hex').slice(0, 20);
9
9
  }
10
+ /** Preserve distinct source IDs; fill aliases only when the producer omitted them. */
11
+ export function normalizeToolCorrelation(input) {
12
+ const callId = compactPart(input.callId);
13
+ const correlationId = compactPart(input.correlationId);
14
+ const requestId = compactPart(input.requestId);
15
+ return {
16
+ callId: callId ?? correlationId ?? requestId,
17
+ correlationId: correlationId ?? callId ?? requestId,
18
+ requestId: requestId ?? callId ?? correlationId,
19
+ };
20
+ }
10
21
  /** Stable across source/mirror records for one tool-call lifecycle. */
11
22
  export function buildToolLifecycleEventKey(input) {
23
+ const { callId } = normalizeToolCorrelation(input);
24
+ // No source ID means no safe join. Never merge unrelated anonymous calls.
25
+ if (!callId)
26
+ return `tool:unlinked:${crypto.randomUUID()}`;
12
27
  return `tool:${stableDigest([
13
28
  input.sessionId ?? 'unknown',
14
- input.callId ?? input.correlationId ?? input.requestId ?? 'unknown',
29
+ callId,
15
30
  ])}`;
16
31
  }
17
32
  /** Stable across repeated projections of one authorization decision. */
18
33
  export function buildAuthorizationEventKey(input) {
19
- const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
34
+ const lifecycleId = normalizeToolCorrelation(input).callId;
20
35
  if (lifecycleId) {
21
36
  return `${buildToolLifecycleEventKey({
22
37
  sessionId: input.sessionId,
@@ -1,6 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { buildToolLifecycleEventKey, normalizeToolCorrelation } from './event-key.js';
4
5
  // Logger rotation is hourly/daily, but the suffix is intentionally generic so
5
6
  // this check also covers future rotation formats without changing the audit.
6
7
  const STRUCTURED_LOG_RE = /^(?:daemon|events|channel-in|channel-out|messages|command-audit)(?:-[^.]+)?\.log$/;
@@ -33,6 +34,7 @@ export function inspectStructuredLogs(logDir, options = {}) {
33
34
  // daemon logs mix human-readable entries with structured audit lines.
34
35
  // Ignore the former, but still validate malformed structured payloads.
35
36
  if (MIXED_TEXT_LOG_RE.test(file)) {
37
+ raw = raw.replace(/^\[[^\]]+\] \[(?:DEBUG|INFO|WARN|ERROR)\] /, '');
36
38
  if (raw.startsWith('[CommandAuditRecord] '))
37
39
  raw = raw.slice('[CommandAuditRecord] '.length);
38
40
  else if (!raw.startsWith('{'))
@@ -57,8 +59,9 @@ export function inspectStructuredLogs(logDir, options = {}) {
57
59
  issues.push({ file, line: index + 1, reason: 'missing_agent', key });
58
60
  if (isLifecycleRecord(value) && !hasContextValue(lifecycleField(value, 'permissionMode')))
59
61
  issues.push({ file, line: index + 1, reason: 'missing_permission_mode', key });
60
- if (isCorrelatableRecord(value) && !key)
62
+ if (isCorrelatableRecord(value) && (!key || value.correlationMissing === true || key.startsWith('tool:unlinked:'))) {
61
63
  issues.push({ file, line: index + 1, reason: 'missing_correlation' });
64
+ }
62
65
  });
63
66
  }
64
67
  const seen = new Set();
@@ -79,16 +82,33 @@ export function inspectStructuredLogs(logDir, options = {}) {
79
82
  function structuredRecordKey(value) {
80
83
  const nested = nestedRecord(value);
81
84
  const eventKey = firstDefined(value, nested, ['eventKey', 'event_key']);
82
- const eventPhase = firstDefined(value, nested, ['eventPhase', 'event_phase']);
85
+ const eventPhase = recordPhase(value);
83
86
  if (eventKey)
84
87
  return `${String(eventKey)}${eventPhase ? `:${String(eventPhase)}` : ''}`;
88
+ if (isLifecycleRecord(value)) {
89
+ const correlation = normalizeToolCorrelation({
90
+ callId: firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
91
+ correlationId: firstDefined(value, nested, ['correlationId', 'correlation_id']),
92
+ requestId: firstDefined(value, nested, ['requestId', 'request_id']),
93
+ });
94
+ if (!correlation.callId)
95
+ return undefined;
96
+ return `${buildToolLifecycleEventKey({
97
+ sessionId: lifecycleField(value, 'sessionId'),
98
+ ...correlation,
99
+ })}:${eventPhase}`;
100
+ }
85
101
  const correlation = firstDefined(value, nested, [
86
102
  'correlationId', 'correlation_id', 'callId', 'call_id', 'toolUseId', 'tool_use_id',
87
103
  'requestId', 'request_id', 'msgId', 'operationId',
88
104
  ]);
89
105
  if (!correlation)
90
106
  return undefined;
91
- return `${canonicalEventType(value)}:${String(correlation)}`;
107
+ return JSON.stringify([lifecycleField(value, 'sessionId'), canonicalEventType(value), correlation, eventPhase]);
108
+ }
109
+ function recordPhase(value) {
110
+ return firstDefined(value, nestedRecord(value), ['eventPhase', 'event_phase'])
111
+ ?? (canonicalEventType(value) === 'tool_use' ? 'use' : canonicalEventType(value) === 'tool_result' ? 'result' : undefined);
92
112
  }
93
113
  function isLifecycleRecord(value) {
94
114
  const type = canonicalEventType(value);
@@ -101,11 +121,13 @@ function isCorrelatableRecord(value) {
101
121
  }
102
122
  function stableHash(value) {
103
123
  const nested = nestedRecord(value);
104
- const eventKey = firstDefined(value, nested, ['eventKey', 'event_key']);
124
+ const eventKey = structuredRecordKey(value);
105
125
  const canonical = {
106
126
  eventKey,
107
- eventPhase: firstDefined(value, nested, ['eventPhase', 'event_phase']),
127
+ eventPhase: recordPhase(value),
108
128
  type: canonicalEventType(value),
129
+ operation: firstDefined(value, nested, ['operation']),
130
+ targetAgentAid: firstDefined(value, nested, ['targetAgentAid']),
109
131
  toolName: firstDefined(value, nested, ['toolName', 'tool', 'name']),
110
132
  callId: eventKey ? undefined : firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
111
133
  correlationId: eventKey ? undefined : firstDefined(value, nested, ['correlationId', 'correlation_id']),
@@ -116,11 +138,15 @@ function stableHash(value) {
116
138
  isError: firstDefined(value, nested, ['isError', 'is_error']) ?? (value.ok === false || nested?.ok === false ? true : undefined),
117
139
  errorCode: firstDefined(value, nested, ['errorCode', 'error_code']),
118
140
  decision: firstDefined(value, nested, ['decision']),
141
+ decisionSource: firstDefined(value, nested, ['decisionSource', 'decision_source']),
142
+ policyCode: firstDefined(value, nested, ['policyCode', 'policy_code']),
119
143
  executed: firstDefined(value, nested, ['executed']),
120
144
  executionState: firstDefined(value, nested, ['executionState', 'execution_state']),
121
- input: firstDefined(value, nested, ['input']),
122
- result: firstDefined(value, nested, ['result', 'content']),
123
- error: firstDefined(value, nested, ['error', 'errorMessage']),
145
+ // A lifecycle stage may be mirrored without its large input/output body.
146
+ // Keep decision/classification differences, not projection payload size.
147
+ input: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['input']),
148
+ result: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['result', 'content']),
149
+ error: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['error', 'errorMessage']),
124
150
  };
125
151
  return crypto.createHash('sha256').update(JSON.stringify(canonical)).digest('hex').slice(0, 16);
126
152
  }