evolcore 0.0.31 → 0.0.33

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 (179) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/install-codex-managed-hooks.mjs +124 -37
  3. package/dist/agents/claude-runner.js +29 -2
  4. package/dist/agents/codex/app-server-client.js +3 -0
  5. package/dist/agents/codex/diagnostics.js +76 -44
  6. package/dist/agents/codex-runner.js +51 -24
  7. package/dist/agents/ecagent-runner.js +9 -13
  8. package/dist/agents/gemini-runner.js +4 -0
  9. package/dist/aun/aid/store.js +0 -81
  10. package/dist/aun/group/fs-download.js +1 -10
  11. package/dist/aun/group/operations.js +0 -1
  12. package/dist/aun/msg/history.js +1 -0
  13. package/dist/aun/msg/managed-operation.js +130 -32
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/aun/service-proxy.js +12 -18
  17. package/dist/aun/storage/index.js +29 -0
  18. package/dist/channels/aun.js +38 -137
  19. package/dist/channels/daemon.js +257 -2
  20. package/dist/cli/agent-command.js +64 -31
  21. package/dist/cli/agent-create.js +43 -24
  22. package/dist/cli/agent.js +16 -70
  23. package/dist/cli/aun-commands.js +512 -32
  24. package/dist/cli/contact.js +8 -2
  25. package/dist/cli/ctl-command.js +1 -2
  26. package/dist/cli/daemon-commands.js +82 -13
  27. package/dist/cli/debug-mode.js +154 -0
  28. package/dist/cli/diag.js +2 -2
  29. package/dist/cli/handoff-command.js +18 -5
  30. package/dist/cli/index.js +81 -64
  31. package/dist/cli/init.js +2 -2
  32. package/dist/cli/managed-command-guard.js +5 -0
  33. package/dist/cli/memory-command.js +93 -12
  34. package/dist/cli/model.js +14 -138
  35. package/dist/cli/queue-command.js +51 -0
  36. package/dist/cli/response.js +5 -54
  37. package/dist/cli/session-command.js +194 -0
  38. package/dist/cli/skill-command.js +85 -3
  39. package/dist/cli/stats.js +10 -4
  40. package/dist/cli/task-command.js +1 -0
  41. package/dist/cli/task-context.js +16 -0
  42. package/dist/cli/thread-command.js +620 -0
  43. package/dist/cli/trigger-command.js +76 -50
  44. package/dist/config/builtin-roles.js +9 -7
  45. package/dist/config/config-field-policy.js +2 -3
  46. package/dist/config/config-manager.js +7 -2
  47. package/dist/config/resolved-config-op.js +3 -0
  48. package/dist/config-store.js +15 -0
  49. package/dist/core/agent-ipc-registration.js +65 -22
  50. package/dist/core/auth/agent-delegation.js +42 -2
  51. package/dist/core/auth/debug-context.js +139 -0
  52. package/dist/core/auth/delegation-authorization.js +1 -0
  53. package/dist/core/auth/operation-authorizer.js +24 -1
  54. package/dist/core/auth/operation-catalog.js +44 -3
  55. package/dist/core/background-session/session-cleanup.js +212 -0
  56. package/dist/core/background-session/types.js +10 -0
  57. package/dist/core/bootstrap-messages.js +1 -1
  58. package/dist/core/bootstrap-service.js +6 -3
  59. package/dist/core/command/command-handler.js +224 -45
  60. package/dist/core/command/diag-service.js +5 -1
  61. package/dist/core/command/memory-slash.js +6 -2
  62. package/dist/core/command/menu-catalog.js +48 -1
  63. package/dist/core/command/menu-handler.js +92 -16
  64. package/dist/core/command/menu-protocol.js +24 -1
  65. package/dist/core/command/skill-slash.js +322 -0
  66. package/dist/core/command/slash-gate.js +6 -4
  67. package/dist/core/command/slash-handler.js +193 -13
  68. package/dist/core/command/topic-slash.js +528 -0
  69. package/dist/core/evolagent-registry.js +20 -5
  70. package/dist/core/handoff/ipc-registration.js +38 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +210 -9
  74. package/dist/core/memory/consolidation-context.js +49 -16
  75. package/dist/core/memory/consolidation-ops-records.js +35 -0
  76. package/dist/core/memory/consolidation-queue-format.js +21 -0
  77. package/dist/core/memory/consolidation.js +174 -180
  78. package/dist/core/message/aun-ipc-sender.js +207 -8
  79. package/dist/core/message/im-renderer.js +149 -6
  80. package/dist/core/message/message-bridge.js +13 -3
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +131 -3
  83. package/dist/core/message/peer-mode.js +6 -1
  84. package/dist/core/message/response-engine.js +379 -69
  85. package/dist/core/message/subtask-tool-projection.js +140 -0
  86. package/dist/core/model/catalog-service.js +17 -12
  87. package/dist/core/permission/ec-command-parser.js +126 -7
  88. package/dist/core/permission/sandbox-runtime.js +14 -1
  89. package/dist/core/session/session-idle-monitor.js +249 -22
  90. package/dist/core/session/session-manager.js +54 -9
  91. package/dist/core/skill/command-parser.js +13 -0
  92. package/dist/core/skill/index-generator.js +4 -1
  93. package/dist/core/skill/startup.js +41 -0
  94. package/dist/core/skill/switch-resolver.js +55 -29
  95. package/dist/core/task/task-recovery-service.js +2 -0
  96. package/dist/core/task/task-resume-operation.js +11 -0
  97. package/dist/core/thread/command-parser.js +125 -0
  98. package/dist/core/thread/index.js +1 -0
  99. package/dist/core/thread/ipc-operation.js +95 -0
  100. package/dist/core/thread/thread-context-storage.js +131 -0
  101. package/dist/core/thread/thread-operation.js +258 -0
  102. package/dist/core/thread/thread-route.js +33 -0
  103. package/dist/core/thread/thread-service.js +777 -0
  104. package/dist/core/thread/types.js +1 -0
  105. package/dist/eck/message-renderer.js +35 -0
  106. package/dist/index.js +10 -7
  107. package/dist/ipc.js +234 -17
  108. package/dist/paths.js +23 -0
  109. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  110. package/dist/trigger/authorization.js +59 -29
  111. package/dist/trigger/feedback.js +7 -1
  112. package/dist/trigger/ipc-service.js +49 -35
  113. package/dist/trigger/parser.js +63 -1
  114. package/dist/trigger/script-sandbox.js +58 -77
  115. package/dist/trigger/validation.js +14 -0
  116. package/dist/utils/error-utils.js +22 -1
  117. package/kits/docs/AUTHORING.md +1 -1
  118. package/kits/docs/channels/aun.md +8 -0
  119. package/kits/docs/context-assembly.md +1 -0
  120. package/kits/docs/evolcore/INDEX.md +3 -2
  121. package/kits/docs/evolcore/agent-create.md +39 -28
  122. package/kits/docs/evolcore/agent.md +19 -4
  123. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  124. package/kits/docs/evolcore/group-collaboration.md +3 -5
  125. package/kits/docs/evolcore/group.md +3 -4
  126. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  127. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  128. package/kits/docs/evolcore/menu-protocol.md +7 -0
  129. package/kits/docs/evolcore/msg.md +5 -8
  130. package/kits/docs/evolcore/task.md +2 -0
  131. package/kits/docs/evolcore/thread-chain.md +267 -0
  132. package/kits/docs/evolcore/trigger.md +2 -2
  133. package/kits/docs/prompt-loading-architecture.md +1 -0
  134. package/kits/eck_manifest.json +61 -26
  135. package/kits/eck_message_manifest.json +169 -10
  136. package/kits/rules/01-overview.md +0 -9
  137. package/kits/rules/05-venue.md +1 -1
  138. package/kits/rules/06-channel.md +7 -1
  139. package/kits/rules/07-memory-and-thread.md +202 -0
  140. package/kits/schemas/relation-config.schema.9.json +18 -3
  141. package/kits/schemas/role-config.schema.1.json +6 -6
  142. package/kits/schemas/single-session.schema.1.json +6 -6
  143. package/kits/schemas/single-session.schema.2.json +6 -6
  144. package/kits/schemas/single-session.schema.3.json +6 -6
  145. package/kits/skills/.switches.json +12 -1
  146. package/kits/skills/ec-group/SKILL.md +234 -0
  147. package/kits/skills/ec-skill/SKILL.md +10 -1
  148. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  149. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  150. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  151. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  152. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  153. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  154. package/kits/skills/thread-ops/SKILL.md +492 -0
  155. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  156. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  157. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  158. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  159. package/kits/templates/bootstrap-welcome.md +7 -7
  160. package/kits/templates/memory-consolidation/forget.md +5 -0
  161. package/kits/templates/memory-consolidation/generalize.md +5 -0
  162. package/kits/templates/memory-consolidation/profile.md +5 -0
  163. package/kits/templates/memory-consolidation/promote.md +5 -0
  164. package/kits/templates/message-fragments/item.md +2 -1
  165. package/kits/templates/message-fragments/thread-context.md +6 -0
  166. package/kits/templates/message-fragments/trigger-session.md +2 -0
  167. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  168. package/kits/templates/roles/member.json +2 -0
  169. package/kits/templates/roles/visitor.json +2 -0
  170. package/kits/templates/system-fragments/bootstrap.md +66 -72
  171. package/kits/templates/system-fragments/channel.md +3 -6
  172. package/kits/templates/system-fragments/commands.md +2 -1
  173. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  174. package/kits/templates/system-fragments/reply.md +3 -2
  175. package/kits/templates/system-fragments/session.md +5 -14
  176. package/package.json +3 -1
  177. package/kits/docs/evolcore/group-fs.md +0 -17
  178. package/kits/docs/venues/aun-group.md +0 -45
  179. package/kits/docs/venues/group.md +0 -43
@@ -21,6 +21,20 @@ export class MessageQueue {
21
21
  interruptBarriers = new Map();
22
22
  interruptFailures = new Map();
23
23
  activeTerminalReasons = new Map();
24
+ /**
25
+ * 允许「未提交输入」回队的 key(仅新消息到达触发的中断)。
26
+ * 与 activeTerminalReasons 分开:后者还承载 stop/timeout/priority 抢占等
27
+ * 语义,那些场景绝不能复活旧输入。
28
+ */
29
+ requeueableInterrupts = new Set();
30
+ /**
31
+ * queueKey → 触发本次 new_message 中断的消息。
32
+ *
33
+ * 回队前用它把「打断消息」从 pending 中排除:只有当队列里没有其它等待项时
34
+ * 才回队。否则重放旧输入会插到那些等待项(如被 defer 的群聊消息)前面,
35
+ * 打乱既有调度顺序。
36
+ */
37
+ interruptingMessages = new Map();
24
38
  eventBus;
25
39
  recentMessageIds = new Set();
26
40
  DEDUP_WINDOW = 60_000; // 1 分钟窗口
@@ -108,6 +122,19 @@ export class MessageQueue {
108
122
  }
109
123
  }
110
124
  });
125
+ subscribe.call(eventBus, 'input_accepted', event => {
126
+ if (!('inputId' in event) || !event.inputId)
127
+ return;
128
+ const inputEvent = event;
129
+ // inputId 格式为 sessionId 或 sessionId:xxx,取第一段匹配
130
+ const sessionId = inputEvent.inputId.split(':')[0];
131
+ for (const [queueKey, state] of this.activeStates) {
132
+ if (this.matchesSession(queueKey, sessionId)) {
133
+ state.inputSubmitted = true;
134
+ logger.debug(`[MessageQueue] input_accepted for ${queueKey}, taskId=${state.taskId}, generation=${state.generation}`);
135
+ }
136
+ }
137
+ });
111
138
  subscribe.call(eventBus, 'task:completed', event => {
112
139
  if ('sessionId' in event && event.sessionId)
113
140
  this.markActiveTerminal(event.sessionId, 'completed');
@@ -124,6 +151,37 @@ export class MessageQueue {
124
151
  this.activeTerminalReasons.set(queueKey, reason);
125
152
  }
126
153
  }
154
+ /**
155
+ * 显式允许/禁止该 key 的 active 批次在中断后回队。
156
+ *
157
+ * 只有「新消息到达触发的中断」才置 true:那种场景下旧输入尚未提交给模型,
158
+ * 用户也确实期望两条一起被看到。stop / 优先级抢占 / 超时等一律不允许复活。
159
+ */
160
+ markRequeueable(queueKey, requeueable) {
161
+ if (requeueable)
162
+ this.requeueableInterrupts.add(queueKey);
163
+ else
164
+ this.requeueableInterrupts.delete(queueKey);
165
+ }
166
+ /**
167
+ * 标记 active 批次的输入已真正提交给模型(runner 发出 input_accepted 后调用)。
168
+ *
169
+ * 这是 pre-submit 回队的关键判据:只有「未提交」的 active 才能在 new_message
170
+ * 中断后安全放回队列;已提交的批次若回队,会在 provider session 里产生重复用户轮次。
171
+ */
172
+ markInputSubmitted(sessionId, taskId) {
173
+ for (const [queueKey, state] of this.activeStates) {
174
+ if (!this.matchesSession(queueKey, sessionId))
175
+ continue;
176
+ // 同一 session 可能有多个 queueKey(不同 agent/project),taskId 用来精确定位。
177
+ if (taskId !== undefined && state.taskId !== undefined && state.taskId !== taskId)
178
+ continue;
179
+ state.inputSubmitted = true;
180
+ state.submittedMessageIds = Array.from(state.messageIds);
181
+ logger.debug(`[Queue] input submitted: key=${queueKey} task=${taskId ?? '<none>'} ids=${state.submittedMessageIds.join(',')}`);
182
+ return;
183
+ }
184
+ }
127
185
  /**
128
186
  * 注册一次性消息拦截器:下一条来自 sessionKey 的消息不入队、不触发 interrupt,
129
187
  * 直接传给 handler。用于 AskUserQuestion 的"手动输入"场景。
@@ -1082,7 +1140,12 @@ export class MessageQueue {
1082
1140
  activeState?.peerId === message.peerId &&
1083
1141
  !this.hasOtherPeerQueued(queueKey, message.peerId);
1084
1142
  if (interruptSingle || interruptGroupSamePeer) {
1085
- logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt (samePeer=${interruptGroupSamePeer})`);
1143
+ const inputSubmitted = activeState?.inputSubmitted ?? false;
1144
+ logger.info(`[Queue] new_message interrupt: key=${queueKey} samePeer=${interruptGroupSamePeer} taskId=${activeState?.taskId ?? '<none>'} generation=${activeState?.generation ?? '<none>'} inputSubmitted=${inputSubmitted} activeMsg=${Array.from(activeState?.messageIds ?? []).join(',')}`);
1145
+ // 新消息到达才允许 pre-submit 回队:旧输入尚未提交给模型,
1146
+ // 且用户期望它与刚到的消息一起被看到。
1147
+ this.markRequeueable(queueKey, true);
1148
+ this.interruptingMessages.set(queueKey, message);
1086
1149
  this.eventBus?.publish({
1087
1150
  type: 'task:interrupted',
1088
1151
  sessionId: sessionKey,
@@ -1218,7 +1281,7 @@ export class MessageQueue {
1218
1281
  this.activeBatches.set(queueKey, activeItem);
1219
1282
  this.processingStartTime.set(queueKey, Date.now());
1220
1283
  this.persistQueuesImmediate();
1221
- logger.info(`[Queue] processing batch: key=${queueKey} items=${rawItems.length} pending=${queue.length} rawPending=${this.queueRawMessageCount(queue)} batchRole=${merged.message.batchRole ?? '<mixed>'} peer=${activeState.peerId ?? '<multi>'} msg=${merged.message.messageId ?? '<none>'}`);
1284
+ logger.info(`[Queue] processing batch: key=${queueKey} items=${rawItems.length} pending=${queue.length} rawPending=${this.queueRawMessageCount(queue)} batchRole=${merged.message.batchRole ?? '<mixed>'} peer=${activeState.peerId ?? '<multi>'} msg=${merged.message.messageId ?? '<none>'} sourceIds=${Array.from(activeMessageIds).join(',')}`);
1222
1285
  try {
1223
1286
  await this.handler(merged.message);
1224
1287
  logger.debug(`[Queue] Message processed successfully`);
@@ -1230,10 +1293,17 @@ export class MessageQueue {
1230
1293
  }
1231
1294
  finally {
1232
1295
  if (!this.shuttingDown) {
1296
+ const terminalReason = this.activeTerminalReasons.get(queueKey);
1297
+ const disposition = this.disposeInterruptedBatch(queueKey, activeState, rawParts, merged.message.causation);
1298
+ if (disposition !== 'none') {
1299
+ logger.info(`[Queue] batch disposition: key=${queueKey} reason=${terminalReason ?? '<completed>'} disposition=${disposition}`);
1300
+ }
1233
1301
  this.activeBatches.delete(queueKey);
1234
1302
  this.processingStartTime.delete(queueKey);
1235
1303
  this.activeStates.delete(queueKey);
1236
1304
  this.activeTerminalReasons.delete(queueKey);
1305
+ this.requeueableInterrupts.delete(queueKey);
1306
+ this.interruptingMessages.delete(queueKey);
1237
1307
  this.persistQueuesImmediate();
1238
1308
  }
1239
1309
  }
@@ -1244,6 +1314,63 @@ export class MessageQueue {
1244
1314
  }
1245
1315
  }
1246
1316
  }
1317
+ /**
1318
+ * 决定被中断的 active 批次的下落。
1319
+ *
1320
+ * 只有同时满足两个条件才回队:
1321
+ * 1) 该 key 被显式标记为「新消息到达」(markRequeueable),而不是 stop/超时/优先级抢占;
1322
+ * 2) 该批次的输入尚未提交给模型(未收到 input_accepted / 未产出输出)。
1323
+ *
1324
+ * 已提交的批次若回队,会在 provider session 里产生重复用户轮次;
1325
+ * stop/timeout/recalled 是用户或系统的明确意图,绝不复活;
1326
+ * daemon_restart 由启动恢复流程处理,这里不重复入队。
1327
+ *
1328
+ * 回队到物理队首并保留原始 messageId/receivedAt,下一次 dequeueGreedy 会把它
1329
+ * 与期间到达的 pending 消息一起合并,等价于「未提交的旧输入 + 新输入」同批提交。
1330
+ */
1331
+ disposeInterruptedBatch(queueKey, activeState, rawParts, causation) {
1332
+ if (!this.requeueableInterrupts.has(queueKey))
1333
+ return 'none';
1334
+ if (!this.interruptCallback
1335
+ || this.activeTerminalReasons.get(queueKey) !== 'new_message')
1336
+ return 'none';
1337
+ if (rawParts.length === 0)
1338
+ return 'none';
1339
+ // 已提交:不能重放,交由 provider/session 侧的既有用户轮次保证上下文完整。
1340
+ if (activeState.inputSubmitted)
1341
+ return 'input_already_submitted';
1342
+ const queue = this.queues.get(queueKey);
1343
+ if (!queue) {
1344
+ logger.warn(`[Queue] pre-submit requeue skipped: queue gone key=${queueKey}`);
1345
+ return 'none';
1346
+ }
1347
+ const requeued = rawParts.map(part => this.itemFromPart(part));
1348
+ // 只有当队列里除打断消息外没有其它等待项时才回队。
1349
+ // 若已有其它 pending(例如被 defer 的群聊消息),说明旧输入本可在当前轮
1350
+ // 处理完毕,回队会插入一个与它们无关的额外轮次并打乱既有调度顺序;
1351
+ // 那种情况下保持原有中断行为。
1352
+ const interrupting = this.interruptingMessages.get(queueKey);
1353
+ const pendingWithoutInterrupt = interrupting
1354
+ ? queue.filter(item => item.message.messageId !== interrupting.messageId)
1355
+ : queue;
1356
+ if (pendingWithoutInterrupt.length > 0) {
1357
+ logger.info(`[Queue] pre-submit requeue skipped: key=${queueKey} other pending=${pendingWithoutInterrupt.length}`);
1358
+ return 'none';
1359
+ }
1360
+ queue.unshift(...requeued);
1361
+ // 按物理顺序重建逻辑队列,避免把旧输入追加到新消息之后,导致 FIFO
1362
+ // 在下一次 reorderPhysical 时颠倒回队消息与打断消息的顺序。
1363
+ // 与 enqueue 路径保持同一条件,避免把 owner-inject/interrupt 消息写进逻辑队列。
1364
+ this.logicalQueue.clear(queueKey);
1365
+ for (const item of queue) {
1366
+ if (item.message.source !== 'owner-inject' && item.message.attentionPolicy !== 'interrupt') {
1367
+ this.logicalQueue.enqueue(queueKey, item.message);
1368
+ }
1369
+ }
1370
+ logger.info(`[Queue] requeued pre-submit batch: key=${queueKey} ids=${requeued
1371
+ .flatMap(item => this.messageIdsFor(item)).join(',')} causation=${causation?.traceId ?? '<none>'}`);
1372
+ return 'requeued_pre_submit';
1373
+ }
1247
1374
  /**
1248
1375
  * 贪心弹出队首可合并消息。
1249
1376
  * 群聊入队携带 role,此时取出当前所有群聊 pending 并按时间线合并;
@@ -1337,7 +1464,8 @@ export class MessageQueue {
1337
1464
  && a.channelKey === b.channelKey
1338
1465
  && a.channelType === b.channelType
1339
1466
  && a.chatType === b.chatType
1340
- && a.peerKey === b.peerKey;
1467
+ && a.peerKey === b.peerKey
1468
+ && a.approvalInteractionPolicy === b.approvalInteractionPolicy;
1341
1469
  }
1342
1470
  firstTimestamp(item) {
1343
1471
  const parts = this.partsOf(item);
@@ -14,8 +14,11 @@ export function resolveChatMode(params) {
14
14
  configured = undefined;
15
15
  }
16
16
  const kind = normalizePeerType(params.peerType);
17
+ // 运行时硬约束(非配置项):system/service 始终 interactive;agent 始终 proactive
17
18
  if (kind === 'system' || kind === 'service')
18
19
  return 'interactive';
20
+ if (kind === 'agent')
21
+ return 'proactive';
19
22
  const field = chatmodeFieldForPeer(params.chatType, params.peerType);
20
23
  const decision = resolveRuntimeStringField({
21
24
  selfAid: params.self,
@@ -70,9 +73,11 @@ export function isSystemOrServicePeer(peerType) {
70
73
  */
71
74
  export function resolveChatModeForPeer(params) {
72
75
  const kind = normalizePeerType(params.peerType);
73
- // 运行时硬约束(非配置项):system/service 对端始终同步交互
76
+ // 运行时硬约束(非配置项):system/service 对端始终同步交互;agent 对端始终主动响应
74
77
  if (kind === 'system' || kind === 'service')
75
78
  return 'interactive';
79
+ if (kind === 'agent')
80
+ return 'proactive';
76
81
  // 合并后的场景表 → 出厂默认表
77
82
  const field = chatmodeFieldForPeer(params.chatType, params.peerType);
78
83
  return params.configured?.[field] ?? chatmodeFactoryDefaults()[field];