evolcore 0.0.22 → 0.0.24

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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -325,44 +325,71 @@ function withFallbackModel(override, candidate) {
325
325
  ...(candidate.effort ? { effort: candidate.effort } : { effortMode: 'model_default' }),
326
326
  };
327
327
  }
328
+ /**
329
+ * Keep the per-task fullaccess fact in the user-message prompt rather than in
330
+ * the long-lived system/developer prompt. This avoids changing a runner's
331
+ * system-prompt cache while still giving the model an explicit, auditable
332
+ * execution-mode signal for the privileged task.
333
+ */
334
+ const FULL_ACCESS_MESSAGE_PROMPT_CONTEXT = '【当前权限模式提升为fullaccess,可不受限制执行命令,仅当前轮 task 有效】';
335
+ /**
336
+ * Put the one-task permission notice between the rendered message envelope and
337
+ * the user's literal input. The normal message fragment ends its envelope in
338
+ * `›`; matching the dated normal-message form avoids placing the notice inside
339
+ * an owner-hint envelope when one is present before the real inbound message.
340
+ */
341
+ function insertMessagePromptContext(prompt, context, userContent) {
342
+ if (!context)
343
+ return prompt;
344
+ const headers = [...prompt.matchAll(/‹\d{4}-\d{2}-\d{2} [^\n]*· from:[^\n]*›/gu)];
345
+ const renderedHeader = headers[0];
346
+ const headerEnd = renderedHeader
347
+ ? (renderedHeader.index ?? 0) + renderedHeader[0].length
348
+ : -1;
349
+ if (headerEnd >= 0) {
350
+ const remainder = prompt.slice(headerEnd).replace(/^\n+/u, '');
351
+ return `${prompt.slice(0, headerEnd)}\n${context}${remainder ? `\n${remainder}` : ''}`;
352
+ }
353
+ if (userContent?.trim()) {
354
+ const contentStart = prompt.indexOf(userContent);
355
+ if (contentStart >= 0) {
356
+ return `${prompt.slice(0, contentStart)}${context}\n${prompt.slice(contentStart)}`;
357
+ }
358
+ }
359
+ // Internal recovery prompts have no message envelope; keep their context at
360
+ // the front so the runner still sees it for the same task.
361
+ return `${context}\n\n${prompt}`;
362
+ }
328
363
  function getStreamErrorText(result) {
329
- return [
364
+ const parts = [
330
365
  result.errors?.join('\n'),
331
- result.lastReplyText,
332
- result.fullText,
333
366
  result.subtype,
334
367
  result.terminalReason,
335
368
  ]
336
- .filter((part) => typeof part === 'string' && part.trim().length > 0)
337
- .join('\n');
369
+ .filter((part) => typeof part === 'string' && part.trim().length > 0);
370
+ // Some older runners put the terminal diagnostic in `result`, but never
371
+ // treat streamed assistant text as an error merely because the turn failed.
372
+ const fullText = result.fullText.trim();
373
+ const fullTextLooksLikeDiagnostic = fullText.length > 0 && (isRetryableError(new Error(fullText))
374
+ || isContextTooLongText(fullText));
375
+ if (result.isError && !result.hasReceivedText && fullText
376
+ && (parts.length === 0 || fullTextLooksLikeDiagnostic)) {
377
+ parts.unshift(fullText);
378
+ }
379
+ return parts.join('\n');
338
380
  }
339
381
  function getStreamErrorMessage(result, includeEmoji = false) {
340
382
  const raw = getStreamErrorText(result) || '任务执行失败';
341
383
  const mapped = getErrorMessage(new Error(raw), result.terminalReason, includeEmoji);
342
384
  const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
343
- if (!result.terminalReason && raw && mapped === generic) {
385
+ // Unknown runner-specific terminal reasons do not have a friendly mapping.
386
+ // Preserve their diagnostic text instead of collapsing it to the generic
387
+ // "处理消息时出错" message.
388
+ if (raw && mapped === generic) {
344
389
  return includeEmoji ? `❌ ${raw}` : raw;
345
390
  }
346
391
  return mapped;
347
392
  }
348
- function getRuntimeErrorMessage(raw, includeEmoji = false) {
349
- const fallback = raw || '任务执行失败';
350
- const mapped = getErrorMessage(new Error(fallback), undefined, includeEmoji);
351
- const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
352
- if (mapped === generic)
353
- return includeEmoji ? `❌ ${fallback}` : fallback;
354
- return mapped;
355
- }
356
- function normalizeComparableText(text) {
357
- return text.replace(/\s+/g, ' ').trim();
358
- }
359
- function isPendingTextSameAsStreamError(pendingText, errorText) {
360
- const pending = normalizeComparableText(pendingText);
361
- const error = normalizeComparableText(errorText);
362
- if (!pending || !error || pending.length < 20)
363
- return false;
364
- return pending === error || pending.includes(error) || error.includes(pending);
365
- }
366
393
  function streamHitContextLimit(result) {
367
394
  return result.terminalReason === 'prompt_too_long' ||
368
395
  isContextTooLongText(result.lastReplyText) ||
@@ -446,12 +473,6 @@ function getContextTooLongHint(agent) {
446
473
  }
447
474
  return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
448
475
  }
449
- function getContextCompactFailedHint(agent) {
450
- if (canCompactAgent(agent)) {
451
- return '上下文过长,自动压缩失败,请手动输入 /compact 重试';
452
- }
453
- return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
454
- }
455
476
  function canCompactAgent(agent) {
456
477
  return hasCompact(agent) && agent.capabilities?.compact !== false;
457
478
  }
@@ -496,6 +517,7 @@ export class ResponseEngine {
496
517
  /** 已主动发送开始提示时,忽略 runner 对同一次 compact 的回调。 */
497
518
  announcedCompactStarts = new Set();
498
519
  agentMap;
520
+ legacySingleRunnerMode;
499
521
  primaryRunnerKey;
500
522
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
501
523
  timeoutErrors = new Map();
@@ -531,27 +553,42 @@ export class ResponseEngine {
531
553
  * session/config mismatch and must not silently route to a different backend.
532
554
  */
533
555
  getAgent(channel, baseagent, selfAID) {
534
- if (selfAID && baseagent) {
535
- const key = `${selfAID}::${baseagent}`;
556
+ const requestedBaseagent = typeof baseagent === 'string' ? baseagent.trim() : '';
557
+ // Keep the explicit single-backend map shape usable by embedders and
558
+ // focused tests. This is an exact key lookup, not a cross-agent fallback.
559
+ if (requestedBaseagent && this.agentMap.has(requestedBaseagent)) {
560
+ return this.agentMap.get(requestedBaseagent);
561
+ }
562
+ // A caller that passed one runner explicitly has already selected the
563
+ // backend; this compatibility path does not exist for multi-agent maps.
564
+ if (this.legacySingleRunnerMode && this.agentMap.size === 1
565
+ && this.agentMap.values().next().value?.name === requestedBaseagent) {
566
+ return this.agentMap.values().next().value;
567
+ }
568
+ if (selfAID && requestedBaseagent) {
569
+ const key = `${selfAID}::${requestedBaseagent}`;
536
570
  if (this.agentMap.has(key))
537
571
  return this.agentMap.get(key);
538
572
  }
539
- if (channel && baseagent) {
573
+ if (channel && requestedBaseagent) {
540
574
  const owner = this.agentRegistry?.resolveByChannel(channel);
541
575
  const evolName = owner?.name || '<unknown>';
542
- const key = `${evolName}::${baseagent}`;
576
+ const key = `${evolName}::${requestedBaseagent}`;
543
577
  if (this.agentMap.has(key))
544
578
  return this.agentMap.get(key);
545
- const singleRunnerKey = `<unknown>::${baseagent}`;
546
- if (this.agentMap.has(singleRunnerKey))
547
- return this.agentMap.get(singleRunnerKey);
548
579
  if (owner) {
549
- throw new BaseagentRunnerUnavailableError(evolName, baseagent, this.getAvailableBaseagentsForOwner(evolName));
580
+ throw new BaseagentRunnerUnavailableError(evolName, requestedBaseagent, this.getAvailableBaseagentsForOwner(evolName));
550
581
  }
582
+ // Control/test channels may have no registry owner. Resolve only when
583
+ // exactly one loaded runner advertises this baseagent; never choose the
584
+ // primary runner or the first map entry as a backend substitute.
585
+ const matches = [...this.agentMap.entries()]
586
+ .filter(([runnerKey]) => runnerKey.endsWith(`::${requestedBaseagent}`));
587
+ if (matches.length === 1)
588
+ return matches[0][1];
589
+ throw new BaseagentRunnerUnavailableError('<unknown>', requestedBaseagent, [...new Set(matches.map(([runnerKey]) => runnerKey.slice(runnerKey.lastIndexOf('::') + 2)))]);
551
590
  }
552
- if (this.agentMap.has(this.primaryRunnerKey))
553
- return this.agentMap.get(this.primaryRunnerKey);
554
- return this.agentMap.values().next().value;
591
+ throw new BaseagentRunnerUnavailableError('<unknown>', requestedBaseagent || '<missing>', []);
555
592
  }
556
593
  getTextInferenceProvider(channel, baseagent, selfAID) {
557
594
  if (!baseagent)
@@ -627,11 +664,13 @@ export class ResponseEngine {
627
664
  this.turnCoordinator = new SessionTurnCoordinator(sessionManager);
628
665
  if (agentRunnerOrMap instanceof Map) {
629
666
  this.agentMap = agentRunnerOrMap;
667
+ this.legacySingleRunnerMode = false;
630
668
  this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
631
669
  }
632
670
  else {
633
671
  // 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
634
672
  this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
673
+ this.legacySingleRunnerMode = true;
635
674
  this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
636
675
  }
637
676
  // 监听中断事件,标记被中断的 session
@@ -886,14 +925,10 @@ export class ResponseEngine {
886
925
  if (owning)
887
926
  owning.lastActivity = Date.now();
888
927
  }
889
- inferPrimaryBaseagent() {
890
- const idx = this.primaryRunnerKey.lastIndexOf('::');
891
- return idx >= 0 ? this.primaryRunnerKey.slice(idx + 2) : this.primaryRunnerKey || undefined;
892
- }
893
928
  ensureSessionBaseagent(session, fallback) {
894
- const legacyAgentId = typeof session.agentId === 'string' ? session.agentId : undefined;
895
- if (!session.baseagent) {
896
- session.baseagent = fallback || legacyAgentId || this.inferPrimaryBaseagent();
929
+ void fallback;
930
+ if (typeof session.baseagent !== 'string' || !session.baseagent.trim()) {
931
+ throw new Error(`[ResponseEngine] session ${session.id} has no baseagent binding; refusing implicit fallback`);
897
932
  }
898
933
  }
899
934
  inferDirectSessionIdentity(message, owningAgent) {
@@ -1042,7 +1077,7 @@ export class ResponseEngine {
1042
1077
  return true;
1043
1078
  }
1044
1079
  async retryAfterContextRecovery(prompt, opts) {
1045
- const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, } = opts;
1080
+ const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, messagePromptContext, } = opts;
1046
1081
  if (!session.agentSessionId || !canCompactAgent(agent)) {
1047
1082
  throw new Error('CONTEXT_COMPACT_FAILED');
1048
1083
  }
@@ -1081,7 +1116,7 @@ export class ResponseEngine {
1081
1116
  await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
1082
1117
  await renderer.flush();
1083
1118
  beforeRunQuery?.();
1084
- const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
1119
+ const retryStream = await agent.runQuery(session.id, insertMessagePromptContext(prompt, messagePromptContext), absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
1085
1120
  if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, turnLease.taskId)) {
1086
1121
  throw new Error('RUNNER_START_SUPERSEDED');
1087
1122
  }
@@ -1365,11 +1400,11 @@ export class ResponseEngine {
1365
1400
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
1366
1401
  '/stop', '/pause', '/resume', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check',
1367
1402
  '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
1368
- '/aid', '/upgrade', '/evolagent',
1403
+ '/aid', '/upgrade', '/evolagent', '/pair ',
1369
1404
  ];
1370
1405
  /** 判断消息内容是否为已知命令 */
1371
1406
  isKnownCommand(content) {
1372
- return content === '/s' ||
1407
+ return content === '/s' || content === '/pair' ||
1373
1408
  ResponseEngine.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
1374
1409
  }
1375
1410
  /**
@@ -1626,6 +1661,19 @@ export class ResponseEngine {
1626
1661
  totalToolCalls: result.state.totalToolCalls,
1627
1662
  lastToolName: result.state.lastToolName,
1628
1663
  });
1664
+ const idleNotice = timeoutControl.idleNotice;
1665
+ if (idleNotice) {
1666
+ void Promise.resolve(idleNotice({
1667
+ action: result.action,
1668
+ idleSec: result.idleSec,
1669
+ message: result.message,
1670
+ totalEvents: result.state.totalEvents,
1671
+ totalToolCalls: result.state.totalToolCalls,
1672
+ lastToolName: result.state.lastToolName,
1673
+ })).catch(error => {
1674
+ logger.debug(`[ResponseEngine] Idle notice send failed: ${error instanceof Error ? error.message : String(error)}`);
1675
+ });
1676
+ }
1629
1677
  }
1630
1678
  result = monitor.check();
1631
1679
  }
@@ -2380,8 +2428,10 @@ export class ResponseEngine {
2380
2428
  opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
2381
2429
  }
2382
2430
  opts.metadata = withTaskMetadata(opts.metadata);
2383
- if (payload.kind.startsWith('status.'))
2431
+ // status.notice is a diagnostic observation, not execution activity.
2432
+ if (payload.kind.startsWith('status.') && payload.kind !== 'status.notice') {
2384
2433
  this.touchAgentActivity(channelKey);
2434
+ }
2385
2435
  const enrichedEnvelope = withEnvelopeReplyContext(envelope, opts);
2386
2436
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
2387
2437
  return await adapter.send(enrichedEnvelope, payload);
@@ -2392,6 +2442,19 @@ export class ResponseEngine {
2392
2442
  renderer,
2393
2443
  suppressActivities: shouldSuppress(),
2394
2444
  });
2445
+ // Idle notify/warn are operational diagnostic notices. Keep them on the
2446
+ // current task's renderer so envelope, stale-turn, background, and
2447
+ // channel-specific suppression rules remain identical to other sends.
2448
+ if (timeoutControl) {
2449
+ timeoutControl.idleNotice = async ({ action, idleSec, message: idleMessage, totalEvents, totalToolCalls, lastToolName }) => {
2450
+ await renderer.sendStatusNotice(idleMessage, action === 'warn' ? 'warn' : 'info', `idle-${action}`, {
2451
+ idleSec,
2452
+ totalEvents,
2453
+ totalToolCalls,
2454
+ lastToolName,
2455
+ });
2456
+ };
2457
+ }
2395
2458
  renderer.addLifecycle('started');
2396
2459
  if (isProactive) {
2397
2460
  logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
@@ -2689,6 +2752,7 @@ export class ResponseEngine {
2689
2752
  : body;
2690
2753
  // 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
2691
2754
  let effectivePrompt = wrapPrompt(message.content);
2755
+ let fullAccessMessagePromptContext;
2692
2756
  let renderResult;
2693
2757
  let effectiveSystemPrompt;
2694
2758
  let modelOverride;
@@ -2784,6 +2848,9 @@ export class ResponseEngine {
2784
2848
  // records all observe the same normalized value.
2785
2849
  permissionContext.permissionMode = effectivePermissionMode;
2786
2850
  permissionContext.executionPermission = fullAccessAuthorization;
2851
+ fullAccessMessagePromptContext = fullAccessAuthorization
2852
+ ? FULL_ACCESS_MESSAGE_PROMPT_CONTEXT
2853
+ : undefined;
2787
2854
  // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
2788
2855
  // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
2789
2856
  // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
@@ -2878,7 +2945,9 @@ export class ResponseEngine {
2878
2945
  agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
2879
2946
  primaryModel = modelOverride?.model || agentModel;
2880
2947
  if (!hasExplicitTriggerModel && primaryModel) {
2881
- const fallbackResolution = eligibleFallbackModels({
2948
+ const fallbackResolution = await eligibleFallbackModels({
2949
+ selfAid: selfAid || undefined,
2950
+ catalog: agent.getModelCatalog?.bind(agent),
2882
2951
  baseagent: normalizedBaseagent.canonical,
2883
2952
  currentModel: primaryModel,
2884
2953
  resolveModelId: typeof agent.resolveModelId === 'function'
@@ -3067,6 +3136,7 @@ export class ResponseEngine {
3067
3136
  sessionId: session.id,
3068
3137
  sessionName: session.name || undefined,
3069
3138
  sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
3139
+ managedTempDir: sessionRuntimeDir,
3070
3140
  // 时区(把 ISO 时间戳转本地时间用)+ OS 环境
3071
3141
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
3072
3142
  tzOffset: currentTzOffset(),
@@ -3115,7 +3185,10 @@ export class ResponseEngine {
3115
3185
  try {
3116
3186
  const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
3117
3187
  const cbModel = effectiveModel || agentModel || 'unknown';
3118
- const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
3188
+ // 从统一模型参数表获取模型的真实 context_window
3189
+ const { resolveModelSpec } = await import('../../stats/billing.js');
3190
+ const modelSpec = resolveModelSpec(resolveRoot(), cbModel);
3191
+ const cbMaxTokens = modelSpec.context_window;
3119
3192
  const systemPromptTokens = estTokens(options?.systemPromptAppend);
3120
3193
  const kitTokens = estTokens(kitContext);
3121
3194
  const totalEst = estTokens(effectiveSystemPrompt);
@@ -3218,6 +3291,12 @@ export class ResponseEngine {
3218
3291
  if (v2HandoffIds.length > 0 && renderResult?.body.trim())
3219
3292
  handoffPromptRendered = true;
3220
3293
  }
3294
+ // Keep this notice in the per-turn message prompt. Injecting it into
3295
+ // the system prompt would alter long-lived runner prompt caches and
3296
+ // could make a one-turn grant look persistent.
3297
+ if (fullAccessMessagePromptContext && effectivePrompt.trim()) {
3298
+ effectivePrompt = insertMessagePromptContext(effectivePrompt, fullAccessMessagePromptContext, message.content);
3299
+ }
3221
3300
  // 空消息防护:在 agent 调用之前检查 prompt 是否为空
3222
3301
  // 防止空消息(或纯空格消息)浪费 API 调用
3223
3302
  if (!effectivePrompt.trim()) {
@@ -3282,6 +3361,8 @@ export class ResponseEngine {
3282
3361
  selfAid,
3283
3362
  peerKey,
3284
3363
  issuedRole: peerRole,
3364
+ permissionMode: effectivePermissionMode,
3365
+ ...(sessionRuntimeDir ? { sessionRuntimeDir } : {}),
3285
3366
  ...(fullAccessAuthorization ? { executionIdentity: fullAccessAuthorization } : {}),
3286
3367
  });
3287
3368
  runtimeEnv[AGENT_DELEGATION_TOKEN_ENV] = delegationToken;
@@ -3527,6 +3608,7 @@ export class ResponseEngine {
3527
3608
  proactiveSelfAid: selfAid,
3528
3609
  permissionMode: effectivePermissionMode,
3529
3610
  turnLease,
3611
+ messagePromptContext: fullAccessMessagePromptContext,
3530
3612
  beforeRunQuery: fullAccessAuthorization
3531
3613
  ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3532
3614
  : undefined,
@@ -3573,6 +3655,7 @@ export class ResponseEngine {
3573
3655
  proactiveSelfAid: selfAid,
3574
3656
  permissionMode: effectivePermissionMode,
3575
3657
  turnLease,
3658
+ messagePromptContext: fullAccessMessagePromptContext,
3576
3659
  beforeRunQuery: fullAccessAuthorization
3577
3660
  ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3578
3661
  : undefined,
@@ -4537,7 +4620,7 @@ export class ResponseEngine {
4537
4620
  : undefined;
4538
4621
  const owningAgent = this.agentRegistry?.resolveByChannel(message.channel);
4539
4622
  const projectPath = owningAgent?.projectPath || process.cwd();
4540
- const resolvedBaseagent = message.baseagent || owningAgent?.baseagent || (!this.agentRegistry ? this.inferPrimaryBaseagent() : undefined);
4623
+ const resolvedBaseagent = message.baseagent || owningAgent?.baseagent;
4541
4624
  // 优先复用 MessageBridge 入队前已解析的身份,避免二次独立解析产生角色漂移
4542
4625
  // (曾观测到瞬时竞态下把 owner 误判为 none,导致合法用户被静默拒绝)。
4543
4626
  // 仅当消息不经 MessageBridge.register()(如 trigger 直接投递)时才回退重新解析。
@@ -4727,6 +4810,10 @@ export class ResponseEngine {
4727
4810
  // 每收到事件重置空闲超时
4728
4811
  const toolName = event.type === 'tool_use' ? event.name : undefined;
4729
4812
  resetTimer(event.type, toolName);
4813
+ // Provider reasoning/thinking is an internal liveness signal only.
4814
+ // It must never be projected as progress, activity, text, or transcript.
4815
+ if (event.type === 'heartbeat')
4816
+ continue;
4730
4817
  if (event.type === 'input_accepted') {
4731
4818
  inputAccepted = !turnLease || event.inputId === turnLease.inputId;
4732
4819
  logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
@@ -5254,8 +5341,10 @@ export class ResponseEngine {
5254
5341
  // 运行时错误(Codex: turn.failed / item error)
5255
5342
  if (event.type === 'error') {
5256
5343
  logger.warn(`[ResponseEngine] error event: ${event.errorType}: ${event.error}`);
5257
- // 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
5258
- lastReplyText += event.error || '';
5344
+ // Keep runtime diagnostics separate from assistant prose. Appending
5345
+ // an error to lastReplyText makes the terminal flush treat the error
5346
+ // as user-visible model output and can also make it look like a
5347
+ // duplicate of a pending text event.
5259
5348
  completeResult = {
5260
5349
  ...completeResult,
5261
5350
  isError: true,
@@ -5265,14 +5354,6 @@ export class ResponseEngine {
5265
5354
  lastReplyText,
5266
5355
  fullText: '',
5267
5356
  };
5268
- // 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
5269
- const isContextError = isContextTooLongText(event.error || '');
5270
- const isRetryableRuntimeError = isRetryableError(new Error(event.error || ''));
5271
- const isRecoverableModelError = classifyError(new Error(event.error || '')) === ErrorType.MODEL_UNAVAILABLE;
5272
- if (!isContextError && !isRetryableRuntimeError && !isRecoverableModelError && !hasErrorResult && !shouldSuppress()) {
5273
- hasErrorResult = true;
5274
- renderer.addNotice(getRuntimeErrorMessage(event.error || '任务执行失败', false), 'warn', 'runtime-error', true);
5275
- }
5276
5357
  }
5277
5358
  // 完成事件
5278
5359
  // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
@@ -5291,34 +5372,6 @@ export class ResponseEngine {
5291
5372
  madeProgress = true;
5292
5373
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
5293
5374
  // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
5294
- // 失败且无前置错误输出:显示 errors 摘要
5295
- // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
5296
- // 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
5297
- const interruptReason = this.interruptedSessions.get(session.id);
5298
- const isUserInterrupt = isNonErrorInterruptReason(interruptReason)
5299
- || isExpectedTerminalInterrupt(event.terminalReason);
5300
- const isContextTooLong = event.terminalReason === 'prompt_too_long'
5301
- || isContextTooLongText(event.errors?.join(' ') || '')
5302
- || isContextTooLongText(lastReplyText);
5303
- const completeErrorText = getStreamErrorText(completeResult);
5304
- const isRetryableCompleteError = event.isError && !isContextTooLong && completeErrorText
5305
- ? isRetryableError(new Error(completeErrorText))
5306
- : false;
5307
- const isRecoverableModelError = event.isError && !isContextTooLong && completeErrorText
5308
- ? classifyError(new Error(completeErrorText)) === ErrorType.MODEL_UNAVAILABLE
5309
- : false;
5310
- if (event.isError && completeErrorText && isPendingTextSameAsStreamError(renderer.getRemainingText(), completeErrorText)) {
5311
- renderer.discardPendingText();
5312
- }
5313
- if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong && !isRetryableCompleteError && !isRecoverableModelError) {
5314
- // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
5315
- const userFriendlyMessage = event.terminalReason === 'prompt_too_long'
5316
- ? getContextTooLongHint(agent)
5317
- : event.terminalReason === 'context_compact_failed'
5318
- ? getContextCompactFailedHint(agent)
5319
- : getStreamErrorMessage(completeResult, false);
5320
- renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
5321
- }
5322
5375
  // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
5323
5376
  // 最终文本留给流结束后的统一 flush(true)
5324
5377
  if (renderer.hasContent()) {