evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
package/dist/paths.js CHANGED
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
5
  import { DAEMON_CONFIG_FILENAME, DEFAULT_RUNTIME_HOME_DIRNAME, INSTANCE_SOCKET_FILENAME, RUNTIME_HOME_ENV, WINDOWS_PIPE_PREFIX, } from './product.js';
6
+ import { migrateLegacyAgentJsonlLogs } from './core/audit/log-integrity.js';
6
7
  const isWindows = process.platform === 'win32';
7
8
  let _root = null;
8
9
  export function resolveRoot() {
@@ -118,9 +119,6 @@ export function agentVenuesDir(aid) {
118
119
  export function agentSessionsDir(aid) {
119
120
  return path.join(agentDir(aid), 'sessions');
120
121
  }
121
- export function agentLogsDir(aid) {
122
- return path.join(agentDir(aid), 'logs');
123
- }
124
122
  export function agentDataDir(aid) {
125
123
  return path.join(agentDir(aid), 'data');
126
124
  }
@@ -133,6 +131,9 @@ export function agentHandoffDir(aid) {
133
131
  export function agentOutboxPath(aid) {
134
132
  return path.join(agentDataDir(aid), 'outbox.jsonl');
135
133
  }
134
+ export function agentActivityOutboxPath(aid) {
135
+ return path.join(agentDataDir(aid), 'activity-outbox.jsonl');
136
+ }
136
137
  /** Directory-safe channel instance key used by connector runtime state. */
137
138
  function encodePathKey(value) {
138
139
  return encodeURIComponent(value);
@@ -161,11 +162,84 @@ export function channelStatePath(channelKey, ...parts) {
161
162
  export function agentTriggersDir(aid) {
162
163
  return path.join(agentDir(aid), 'triggers');
163
164
  }
164
- function resolveInstanceSocketPath(root) {
165
- if (isWindows) {
166
- const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
167
- return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
165
+ function stripTrailingWindowsSeparators(value) {
166
+ const stripped = value.replace(/[\\/]+$/g, '');
167
+ // Keep a drive-root separator. `C:` is a drive-relative path on Windows.
168
+ if (/^[a-zA-Z]:$/.test(stripped) && /^[a-zA-Z]:[\\/]+$/.test(value)) {
169
+ return `${stripped}\\`;
168
170
  }
171
+ return stripped;
172
+ }
173
+ function normalizeWindowsSocketRoot(root) {
174
+ // Use win32 semantics explicitly: EVOLCORE_HOME can come from Git Bash even
175
+ // when the daemon itself runs under native Windows Node.
176
+ return stripTrailingWindowsSeparators(path.win32.resolve(root)).toLowerCase();
177
+ }
178
+ function windowsSocketPathForRoot(root) {
179
+ const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
180
+ return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
181
+ }
182
+ /**
183
+ * Normalize an IPC endpoint before it reaches node:net.
184
+ *
185
+ * Windows does not support Unix-domain socket files at ordinary filesystem
186
+ * paths. Callers that construct an ad-hoc `.sock` path (tests and embedded
187
+ * runtimes in particular) must therefore use the same deterministic named
188
+ * pipe on both the server and client sides. Existing named pipes and all
189
+ * non-Windows endpoints are preserved verbatim.
190
+ */
191
+ export function normalizeIpcEndpoint(endpoint) {
192
+ // Windows object-manager paths are case-insensitive. Preserve an explicit
193
+ // named pipe verbatim even when a caller spells the namespace as `PIPE`.
194
+ if (!isWindows || endpoint.toLowerCase().startsWith('\\\\.\\pipe\\'))
195
+ return endpoint;
196
+ const normalized = path.win32.resolve(endpoint).toLowerCase();
197
+ const hash = crypto.createHash('sha1').update(normalized).digest('hex').slice(0, 20);
198
+ return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-endpoint-${hash}`;
199
+ }
200
+ /**
201
+ * Return the canonical and pre-normalization Windows daemon pipe names.
202
+ *
203
+ * The extra candidates keep a daemon started by an older EvolCore build
204
+ * reachable while it is still running after the CLI has been upgraded.
205
+ */
206
+ export function getWindowsInstanceSocketPathCandidates(root) {
207
+ const normalized = path.win32.normalize(root);
208
+ const normalizedForward = stripTrailingWindowsSeparators(normalized.replace(/\\/g, '/'));
209
+ const canonical = normalizeWindowsSocketRoot(root);
210
+ const baseVariants = [
211
+ canonical,
212
+ root,
213
+ normalized,
214
+ normalizedForward,
215
+ canonical.replace(/\\/g, '/'),
216
+ root.replace(/\\/g, '/'),
217
+ root.replace(/\//g, '\\'),
218
+ ];
219
+ const variants = new Set();
220
+ for (const variant of baseVariants) {
221
+ const stripped = stripTrailingWindowsSeparators(variant);
222
+ variants.add(variant);
223
+ variants.add(stripped);
224
+ if (stripped) {
225
+ variants.add(`${stripped}\\`);
226
+ variants.add(`${stripped}/`);
227
+ }
228
+ }
229
+ const seen = new Set();
230
+ const candidates = [];
231
+ for (const variant of variants) {
232
+ const socket = windowsSocketPathForRoot(variant);
233
+ if (seen.has(socket))
234
+ continue;
235
+ seen.add(socket);
236
+ candidates.push(socket);
237
+ }
238
+ return candidates;
239
+ }
240
+ function resolveInstanceSocketPath(root) {
241
+ if (isWindows)
242
+ return getWindowsInstanceSocketPathCandidates(root)[0];
169
243
  const filesystemPath = path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
170
244
  // Linux/macOS Unix-domain sockets have a small sockaddr path limit
171
245
  // (typically 108 bytes on Linux). Long managed TMPDIR prefixes can make a
@@ -196,6 +270,7 @@ export function ensureDataDirs() {
196
270
  fs.mkdirSync(p.outboxDir, { recursive: true });
197
271
  fs.mkdirSync(p.eckDir, { recursive: true });
198
272
  fs.mkdirSync(aidsDir(), { recursive: true });
273
+ migrateLegacyAgentJsonlLogs(p.agentsDir, p.logs);
199
274
  migrateFromAun();
200
275
  }
201
276
  const MIGRATION_MARKER = '.migrated-from-aun';
@@ -12,7 +12,6 @@
12
12
  * (Phase 6)真正需要时接入 Runner,当前提供明确的"未实现"占位,避免空壳代码。
13
13
  */
14
14
  import path from 'path';
15
- import fs from 'fs';
16
15
  export class ResponseModeContextBuilder {
17
16
  /** (sessionId::modeId) → sessionState,跨消息保持 */
18
17
  stateCache = new Map();
@@ -22,7 +21,7 @@ export class ResponseModeContextBuilder {
22
21
  */
23
22
  build(modeId, deps) {
24
23
  const sessionState = this.getOrCreateState(deps.session.id, modeId);
25
- const dataDir = this.ensureDataDir(deps.agentDir, modeId);
24
+ const dataDir = path.join(deps.agentDir, 'response-system', modeId);
26
25
  return {
27
26
  session: deps.session,
28
27
  agentConfig: deps.agentConfig,
@@ -63,9 +62,4 @@ export class ResponseModeContextBuilder {
63
62
  }
64
63
  return state;
65
64
  }
66
- ensureDataDir(agentDir, modeId) {
67
- const dir = path.join(agentDir, 'response-system', modeId);
68
- fs.mkdirSync(dir, { recursive: true });
69
- return dir;
70
- }
71
65
  }
@@ -197,6 +197,7 @@ export const proactiveFlow = {
197
197
  const queueMsg = queueLen >= 5
198
198
  ? `⚠️ 有 ${queueLen} 条消息未读,请尽快完成当前任务,或使用 ec ctl queue 读取后同步处理。`
199
199
  : `⚠️ 有 ${queueLen} 条消息未读,可使用 ec ctl queue 读取。`;
200
+ ctx.recordReminder?.({ kind: 'queue_unread', text: queueMsg, queueLength: queueLen });
200
201
  ctx.injectToModel(queueMsg);
201
202
  }
202
203
  }
@@ -208,13 +209,17 @@ export const proactiveFlow = {
208
209
  state.toolReportPending = true;
209
210
  const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
210
211
  const target = state.chatType === 'group' ? '群里' : '对方';
211
- ctx.injectToModel(`⚠️ 工具调用已达到 ${state.toolCount} 次,请立即用 ${cmdHint} 向${target}汇报当前情况和下一步意图。`);
212
+ const reportMsg = `⚠️ 工具调用已达到 ${state.toolCount} 次,请立即用 ${cmdHint} 向${target}汇报当前情况和下一步意图。`;
213
+ ctx.recordReminder?.({ kind: 'tool_report', text: reportMsg, toolCount: state.toolCount });
214
+ ctx.injectToModel(reportMsg);
212
215
  }
213
216
  },
214
217
  // ─── onToolResult:成功发送回执后解除门禁 ───
215
218
  onToolResult(ctx) {
216
219
  const state = ctx.state.get(STATE_KEY);
217
- if (!state || !ctx.isSendCommand(ctx.toolName, ctx.toolInput))
220
+ if (!state)
221
+ return;
222
+ if (!ctx.isSendCommand(ctx.toolName, ctx.toolInput))
218
223
  return;
219
224
  if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error))
220
225
  return;
@@ -5,6 +5,10 @@
5
5
  import { resolvePriceRow, BILLING_FNS } from './billing.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
+ /** Keep wire/log metadata readable while preserving full precision in storage. */
9
+ export function roundCostForOutput(value) {
10
+ return Number.isFinite(value) ? Number(value.toFixed(4)) : value;
11
+ }
8
12
  // 网关价格表缓存(从用户覆盖层 model-prices-gateway.jsonl 读取)
9
13
  let _gatewayPriceCache = null;
10
14
  let _gatewayPriceCacheTs = 0;
@@ -252,6 +252,7 @@ function anomalyClassKey(anomaly) {
252
252
  anomaly.phase,
253
253
  anomaly.toolName,
254
254
  anomaly.policyCode,
255
+ anomaly.decisionSource,
255
256
  anomaly.policy,
256
257
  anomaly.agentAid,
257
258
  anomaly.sessionId,
@@ -37,8 +37,11 @@ export class TriggerFeedbackDispatcher {
37
37
  const result = await this.dispatchDirect(input, renderedText, target);
38
38
  if (result.error)
39
39
  return { status: 'failed', reason: result.error.code, feedback, effects: result.effects, error: result.error };
40
+ const replyStatus = statusFromReply(input.reply);
40
41
  return {
41
- status: statusFromReply(input.reply),
42
+ status: result.effects.some(effect => effect.status === 'queued') && replyStatus !== 'failed'
43
+ ? 'degraded'
44
+ : replyStatus,
42
45
  reason: reasonFromReply(input.reply),
43
46
  feedback,
44
47
  effects: result.effects,
@@ -204,6 +207,7 @@ export class TriggerFeedbackDispatcher {
204
207
  originChannelType: input.trigger.origin?.channelType
205
208
  ?? (input.trigger.origin?.channelKey ? channelTypeFromKey(input.trigger.origin.channelKey) : undefined),
206
209
  originControl: input.trigger.origin?.channelKey === 'daemon',
210
+ authorizedBy: input.trigger.authorizedBy,
207
211
  },
208
212
  };
209
213
  outcome = this.waitForTriggerOutcome(input.runId, input.attemptId, delivery.signal);
@@ -247,12 +251,24 @@ export class TriggerFeedbackDispatcher {
247
251
  };
248
252
  }
249
253
  if (event?.type === 'trigger:skipped') {
254
+ const interruption = event.executionState === 'interrupted' && event.decisionSource === 'infrastructure'
255
+ ? {
256
+ reasonCode: event.reasonCode ?? event.reason,
257
+ decisionSource: event.decisionSource,
258
+ executionState: event.executionState,
259
+ ...(event.generation !== undefined ? { generation: event.generation } : {}),
260
+ ...(event.currentGeneration !== undefined ? { currentGeneration: event.currentGeneration } : {}),
261
+ }
262
+ : undefined;
250
263
  return {
251
- status: 'failed',
264
+ status: interruption ? 'skipped' : 'failed',
252
265
  reason: event.reason,
266
+ ...(interruption ? { interruption } : {}),
253
267
  feedback,
254
268
  effects: [this.inboundEffect('success', target, session.id, startedAt)],
255
- error: { code: event.reason || 'target_session_skipped', message: event.reason || 'target session skipped' },
269
+ error: interruption
270
+ ? null
271
+ : { code: event.reason || 'target_session_skipped', message: event.reason || 'target session skipped' },
256
272
  };
257
273
  }
258
274
  return {
@@ -379,7 +395,7 @@ export class TriggerFeedbackDispatcher {
379
395
  if (isAunTarget && !targetChatType(target)) {
380
396
  throw Object.assign(new Error('AUN trigger feedback requires an explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
381
397
  }
382
- await raceWithAbort(binding.adapter.send(buildEnvelope({
398
+ const receipt = await raceWithAbort(binding.adapter.send(buildEnvelope({
383
399
  taskId: `trigger:${input.runId}`,
384
400
  channel: binding.adapter.channelKey,
385
401
  channelId: target.channelId,
@@ -394,16 +410,60 @@ export class TriggerFeedbackDispatcher {
394
410
  isFinal: true,
395
411
  format: 'plain',
396
412
  }), cancellation.signal);
397
- effects.push({
413
+ const common = {
398
414
  type: 'message.send',
399
- status: 'success',
400
415
  channelKey: target.channelKey,
401
416
  channelType: channelTypeFromKey(target.channelKey),
402
417
  channelId: target.channelId,
403
418
  attempt,
404
419
  startedAt,
405
420
  finishedAt: Date.now(),
406
- });
421
+ };
422
+ if (receipt?.status === 'sent' && receipt.messageId) {
423
+ effects.push({ ...common, status: 'success', operationId: receipt.operationId, messageId: receipt.messageId });
424
+ return { effects, error: null };
425
+ }
426
+ if (receipt?.status === 'queued') {
427
+ effects.push({
428
+ ...common,
429
+ status: 'queued',
430
+ operationId: receipt.operationId,
431
+ outboxId: receipt.outboxId,
432
+ error: receipt.error,
433
+ code: receipt.code,
434
+ });
435
+ return { effects, error: null };
436
+ }
437
+ if (receipt?.status === 'failed') {
438
+ effects.push({
439
+ ...common,
440
+ status: 'failed',
441
+ operationId: receipt.operationId,
442
+ outboxId: receipt.outboxId,
443
+ error: receipt.error,
444
+ code: receipt.code,
445
+ });
446
+ return {
447
+ effects,
448
+ error: {
449
+ code: String(receipt.code ?? 'send_failed'),
450
+ message: receipt.error,
451
+ },
452
+ };
453
+ }
454
+ if (receipt?.status === 'suppressed') {
455
+ const message = `channel suppressed trigger feedback: ${receipt.reason}`;
456
+ effects.push({ ...common, status: 'skipped', error: message, code: 'SEND_SUPPRESSED' });
457
+ return { effects, error: { code: 'send_suppressed', message } };
458
+ }
459
+ if (isAunTarget) {
460
+ const message = 'AUN adapter returned no delivery receipt';
461
+ effects.push({ ...common, status: 'failed', error: message, code: 'MISSING_SEND_RECEIPT' });
462
+ return { effects, error: { code: 'missing_send_receipt', message } };
463
+ }
464
+ // Non-AUN adapters are still allowed to use the legacy void result
465
+ // until their receipt migration is complete.
466
+ effects.push({ ...common, status: 'success' });
407
467
  return { effects, error: null };
408
468
  }
409
469
  catch (err) {
@@ -527,6 +587,9 @@ export class TriggerFeedbackDispatcher {
527
587
  const status = Number(err?.status ?? err?.statusCode ?? err?.code);
528
588
  const rawCode = String(err?.code ?? err?.errorCode ?? '').toLowerCase();
529
589
  const lower = `${message} ${rawCode}`.toLowerCase();
590
+ if (rawCode === 'outbox_full') {
591
+ return { retryable: false, code: 'OUTBOX_FULL', message };
592
+ }
530
593
  if (status === 408 || status === 429 || status >= 500) {
531
594
  return { retryable: true, code: 'send_retryable_error', message };
532
595
  }
@@ -2,7 +2,11 @@ import crypto from 'crypto';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { isManagedSessionRuntimeDir, SESSION_RUNTIME_DIR_ENV } from '../cli/task-context.js';
5
+ import { findMessageLogOperationMessageId } from '../core/message/message-log.js';
6
+ import { chatDirPath } from '../core/session/session-fs-store.js';
7
+ import { resolvePaths } from '../paths.js';
5
8
  import { definitionRevision } from './validation.js';
9
+ import * as aunOutbox from '../aun/outbox.js';
6
10
  export class TriggerHistoryStore {
7
11
  rootDir;
8
12
  agentAid;
@@ -66,7 +70,7 @@ export class TriggerHistoryStore {
66
70
  continue;
67
71
  if (triggerId && event.triggerId !== triggerId)
68
72
  continue;
69
- out.push(event);
73
+ out.push(hydrateHistoryEvent(event));
70
74
  }
71
75
  return out;
72
76
  }
@@ -80,7 +84,7 @@ export class TriggerHistoryStore {
80
84
  if (!event || !isRunEvent(event))
81
85
  continue;
82
86
  if (event.agentAid === this.agentAid && event.triggerId === triggerId) {
83
- out.push(event.audit);
87
+ out.push(hydrateTriggerAudit(event.audit));
84
88
  }
85
89
  }
86
90
  return out;
@@ -95,7 +99,7 @@ export class TriggerHistoryStore {
95
99
  const event = parseHistoryEvent(lines[i], this.agentAid);
96
100
  if (!event || !isRunEvent(event) || !pending.has(event.triggerId))
97
101
  continue;
98
- latest.set(event.triggerId, event.audit);
102
+ latest.set(event.triggerId, hydrateTriggerAudit(event.audit));
99
103
  pending.delete(event.triggerId);
100
104
  }
101
105
  return latest;
@@ -125,7 +129,7 @@ export class TriggerHistoryStore {
125
129
  }
126
130
  if (!isRunEvent(event))
127
131
  continue;
128
- const record = event.audit;
132
+ const record = hydrateTriggerAudit(event.audit);
129
133
  if (record.status !== 'completed'
130
134
  && record.status !== 'degraded'
131
135
  && record.status !== 'noop'
@@ -310,6 +314,77 @@ function finiteNumber(value) {
310
314
  function nonNegativeInteger(value) {
311
315
  return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
312
316
  }
317
+ function hydrateHistoryEvent(event) {
318
+ if (!isRunEvent(event))
319
+ return event;
320
+ return { ...event, audit: hydrateTriggerAudit(event.audit) };
321
+ }
322
+ /**
323
+ * Resolve queued AUN effects against the durable outbox at read time. The
324
+ * original audit is append-only, so this projection is what lets a later
325
+ * expiry become visibly failed without pretending that a missing outbox row
326
+ * was delivered.
327
+ */
328
+ function hydrateTriggerAudit(audit) {
329
+ let changed = false;
330
+ let deliveryFailure;
331
+ const effects = audit.effects.map(effect => {
332
+ if (effect.status !== 'queued' || !effect.outboxId)
333
+ return effect;
334
+ const state = aunOutbox.deliveryState(audit.agentAid, effect.outboxId);
335
+ if (state.status === 'sent') {
336
+ changed = true;
337
+ return { ...effect, status: 'success', messageId: state.messageId };
338
+ }
339
+ if (state.status === 'failed') {
340
+ changed = true;
341
+ deliveryFailure ??= { code: String(state.code ?? 'OUTBOX_DELIVERY_FAILED'), message: state.error };
342
+ return { ...effect, status: 'failed', error: state.error, ...(state.code !== undefined ? { code: state.code } : {}) };
343
+ }
344
+ if (state.status === 'missing' && isAunEffect(effect)) {
345
+ const messageId = findDeliveredEffectMessageId(audit.agentAid, effect);
346
+ changed = true;
347
+ if (messageId) {
348
+ return { ...effect, status: 'success', messageId };
349
+ }
350
+ const code = 'OUTBOX_RESULT_MISSING';
351
+ const error = 'outbox delivery result is missing and no remote message_id was recorded';
352
+ deliveryFailure ??= { code, message: error };
353
+ return { ...effect, status: 'failed', error, code };
354
+ }
355
+ return effect;
356
+ });
357
+ if (!changed)
358
+ return audit;
359
+ if (!deliveryFailure) {
360
+ const hasQueuedEffect = effects.some(effect => effect.status === 'queued');
361
+ const hasFailedEffect = effects.some(effect => effect.status === 'failed');
362
+ const canRestoreReplyStatus = audit.status === 'degraded'
363
+ && !hasQueuedEffect
364
+ && !hasFailedEffect
365
+ && !audit.anomalies?.length;
366
+ const status = canRestoreReplyStatus
367
+ ? audit.reply?.outcome === 'noop' ? 'noop' : 'completed'
368
+ : audit.status;
369
+ return { ...audit, status, effects };
370
+ }
371
+ return {
372
+ ...audit,
373
+ status: 'failed',
374
+ reason: deliveryFailure.code,
375
+ effects,
376
+ error: deliveryFailure,
377
+ };
378
+ }
379
+ function isAunEffect(effect) {
380
+ return effect.channelType === 'aun' || effect.channelKey?.startsWith('aun#') === true;
381
+ }
382
+ function findDeliveredEffectMessageId(agentAid, effect) {
383
+ if (!effect.operationId || !effect.channelId)
384
+ return undefined;
385
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', effect.channelId, agentAid);
386
+ return findMessageLogOperationMessageId(chatDir, effect.operationId);
387
+ }
313
388
  function listAuditLogFiles(logDir) {
314
389
  let entries;
315
390
  try {
@@ -1,13 +1,13 @@
1
- const HISTORY_CHANNELS = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
2
1
  /**
3
2
  * Turns legacy raw session-file references into the controlled history CLI.
4
3
  * The replacement deliberately keeps the explicit peer/channel scope but
5
4
  * never carries a filesystem location into a Trigger prompt.
6
5
  */
7
6
  export function rewriteLegacyTriggerSessionPaths(prompt, triggerAid) {
7
+ const historyChannels = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
8
8
  const legacyPath = /data\/sessions\/([A-Za-z0-9_-]+)\/([^/\s`"'<>]+)(?:\/([^/\s`"'<>]+))?\/messages\.jsonl/g;
9
9
  return prompt.replace(legacyPath, (match, channel, first, second) => {
10
- if (!HISTORY_CHANNELS.has(channel))
10
+ if (!historyChannels.has(channel))
11
11
  return match;
12
12
  const isAun = channel === 'aun';
13
13
  const target = isAun ? second : first;
@@ -2,6 +2,7 @@ import { CronExpressionParser } from 'cron-parser';
2
2
  const TRIGGER_UPDATE_FLAGS = new Set([
3
3
  'once', 'delay', 'at', 'cron', 'every', 'event', 'tz',
4
4
  'prompt', 'name',
5
+ 'script-file',
5
6
  'model', 'effort', 'permission',
6
7
  'max-runs', 'max-duration', 'concurrency', 'missed-policy',
7
8
  ]);
@@ -113,6 +114,7 @@ const PERMISSION_MODES = new Set([
113
114
  'auto',
114
115
  'request',
115
116
  'bypass',
117
+ 'fullaccess',
116
118
  ]);
117
119
  const TRIGGER_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
118
120
  function parsePermissionModeFlag(flags, opts = {}) {
@@ -124,10 +126,10 @@ function parsePermissionModeFlag(flags, opts = {}) {
124
126
  if (raw === 'inherit') {
125
127
  if (opts.allowInherit)
126
128
  return { ok: true, value: null };
127
- return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request 或 bypass' };
129
+ return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request、bypassfullaccess' };
128
130
  }
129
131
  if (!PERMISSION_MODES.has(raw)) {
130
- return { ok: false, error: '--permission 只接受 readonly、auto、request 或 bypass' };
132
+ return { ok: false, error: '--permission 只接受 readonly、auto、request、bypassfullaccess' };
131
133
  }
132
134
  return { ok: true, value: raw };
133
135
  }
@@ -551,7 +553,7 @@ export function parseTriggerUpdateArgv(nameOrId, args, opts = {}) {
551
553
  const parsedFlags = flagsFromArgv(args, 'update');
552
554
  if (!parsedFlags.ok)
553
555
  return { ok: false, error: withPromptFileHint(parsedFlags.error, args, opts) };
554
- const parsed = parseTriggerUpdateFlags(nameOrId, parsedFlags.flags, opts);
556
+ const parsed = parseTriggerUpdateFlags(nameOrId, parsedFlags.flags, { ...opts, allowScriptFile: true });
555
557
  return parsed.ok ? parsed : { ok: false, error: withPromptFileHint(parsed.error, args, opts) };
556
558
  }
557
559
  function flagsFromArgv(args, command) {
@@ -584,6 +586,9 @@ function parseTriggerUpdateFlags(nameOrId, flags, opts = {}) {
584
586
  if (!TRIGGER_UPDATE_FLAGS.has(flag))
585
587
  return { ok: false, error: `update 不支持参数 --${flag}` };
586
588
  }
589
+ if (flags.has('script-file') && !opts.allowScriptFile) {
590
+ return { ok: false, error: '--script-file 仅支持 ec trigger update CLI;请通过受控 CLI 上传脚本文件' };
591
+ }
587
592
  const parsed = commonParsed(flags, { update: true, ...opts });
588
593
  if (!parsed.ok)
589
594
  return parsed;
@@ -606,6 +611,11 @@ function parseTriggerUpdateFlags(nameOrId, flags, opts = {}) {
606
611
  update.name = value.name;
607
612
  if (value.prompt !== undefined)
608
613
  update.prompt = value.prompt;
614
+ const scriptFile = optionalFlagString(flags, 'script-file');
615
+ if ('error' in scriptFile)
616
+ return { ok: false, error: scriptFile.error };
617
+ if (scriptFile.value !== undefined)
618
+ update.scriptFile = scriptFile.value;
609
619
  if (value.model !== undefined)
610
620
  update.model = value.model;
611
621
  if (value.effort !== undefined)
@@ -120,12 +120,12 @@ export class TriggerRuntimeScheduler {
120
120
  }
121
121
  listItems(definitions) {
122
122
  const latestRuns = this.audit.latest(definitions.map(definition => definition.id));
123
- return definitions.map(definition => this.buildListItem(definition, latestRuns.get(definition.id)));
123
+ return definitions.map(definition => this.buildListItem(definition, latestRuns.get(definition.id), this.stats(definition.id)));
124
124
  }
125
125
  listItem(definition) {
126
- return this.buildListItem(definition, this.audit.recent(definition.id, 1)[0]);
126
+ return this.buildListItem(definition, this.audit.recent(definition.id, 1)[0], this.stats(definition.id));
127
127
  }
128
- buildListItem(definition, lastRun) {
128
+ buildListItem(definition, lastRun, stats = this.stats(definition.id)) {
129
129
  const schedule = this.state.readSchedule(definition.id);
130
130
  return {
131
131
  id: definition.id,
@@ -139,6 +139,10 @@ export class TriggerRuntimeScheduler {
139
139
  ...(definition.feedback.target ? { target: definition.feedback.target } : {}),
140
140
  },
141
141
  ...(schedule ? { nextFireAt: schedule.nextFireAt } : {}),
142
+ fireCount: stats.fireCount,
143
+ failCount: stats.failCount,
144
+ ...(stats.lastFiredAt !== undefined ? { lastFiredAt: stats.lastFiredAt } : {}),
145
+ ...(stats.lastResult ? { lastResult: stats.lastResult } : {}),
142
146
  ...(lastRun ? { lastStatus: lastRun.status, lastFinishedAt: lastRun.finishedAt } : {}),
143
147
  updatedAt: definition.updatedAt,
144
148
  };
@@ -627,6 +631,7 @@ export class TriggerRuntimeScheduler {
627
631
  effects: feedbackResult.effects,
628
632
  error: feedbackResult.error,
629
633
  conflictRunId: feedbackResult.conflictRunId,
634
+ interruption: feedbackResult.interruption,
630
635
  causation,
631
636
  });
632
637
  if (!payload.dryRun) {
@@ -1147,6 +1152,7 @@ export class TriggerRuntimeScheduler {
1147
1152
  anomalies,
1148
1153
  effects: feedbackResult.effects,
1149
1154
  error: feedbackResult.error,
1155
+ interruption: feedbackResult.interruption,
1150
1156
  causation,
1151
1157
  });
1152
1158
  this.audit.write(audit);
@@ -1469,6 +1475,7 @@ export class TriggerRuntimeScheduler {
1469
1475
  status: input.status,
1470
1476
  reason: input.reason,
1471
1477
  conflictRunId: input.conflictRunId,
1478
+ ...(input.interruption ? { interruption: input.interruption } : {}),
1472
1479
  ...(activeRun?.executionSessionId ? { executionSessionId: activeRun.executionSessionId } : {}),
1473
1480
  ...(attempts?.length ? { attempts } : {}),
1474
1481
  ...(finalAttempt ? { finalAttempt: finalAttempt.attempt } : {}),
@@ -1783,6 +1790,7 @@ export class TriggerRuntimeScheduler {
1783
1790
  type: 'trigger:skipped',
1784
1791
  ...base,
1785
1792
  reason: audit.reason ?? 'skipped',
1793
+ ...(audit.interruption ?? {}),
1786
1794
  });
1787
1795
  }
1788
1796
  }
@@ -1900,6 +1908,15 @@ function errorReply(runId, durationMs, reason, text) {
1900
1908
  }
1901
1909
  function attemptSummaryFromExecution(attempt, attemptId, execution) {
1902
1910
  if (execution.feedbackResult) {
1911
+ if (execution.feedbackResult.interruption) {
1912
+ return {
1913
+ status: 'interrupted',
1914
+ error: {
1915
+ code: execution.feedbackResult.interruption.reasonCode,
1916
+ message: execution.feedbackResult.reason ?? execution.feedbackResult.interruption.reasonCode,
1917
+ },
1918
+ };
1919
+ }
1903
1920
  const failed = execution.feedbackResult.status !== 'completed';
1904
1921
  return {
1905
1922
  status: failed ? 'failed' : 'completed',
@@ -13,6 +13,7 @@ const PERMISSION_MODES = new Set([
13
13
  'auto',
14
14
  'request',
15
15
  'bypass',
16
+ 'fullaccess',
16
17
  ]);
17
18
  export function normalizeTriggerDefinition(input, opts = {}) {
18
19
  if (!isObject(input))
@@ -234,6 +235,7 @@ function normalizeV3Phase2(raw, opts) {
234
235
  enabled: raw.enabled === undefined ? true : requiredBoolean(raw.enabled, 'enabled'),
235
236
  name,
236
237
  description: optionalString(raw.description),
238
+ authorizedBy: optionalString(raw.authorizedBy),
237
239
  createdAt,
238
240
  updatedAt,
239
241
  origin: normalizeOrigin(raw.origin),
@@ -438,6 +440,9 @@ function normalizeExecution(value, _opts) {
438
440
  onError: normalizeOnError(raw.onError),
439
441
  noopSentinel: optionalString(raw.noopSentinel) ?? DEFAULT_NOOP_SENTINEL,
440
442
  };
443
+ if (execution.type === 'script' && execution.permissionMode === 'fullaccess') {
444
+ throw new Error('script Trigger already executes as the daemon OS user and cannot declare execution.permissionMode=fullaccess');
445
+ }
441
446
  return execution;
442
447
  }
443
448
  function normalizeExecutionBaseagent(value) {
@@ -478,7 +483,7 @@ function normalizeTriggerPermissionMode(value) {
478
483
  if (mode === 'edit' || mode === 'noask' || mode === 'plan') {
479
484
  return normalizePermissionModeContract(mode).mode;
480
485
  }
481
- throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass');
486
+ throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass, fullaccess');
482
487
  }
483
488
  function normalizeScript(value) {
484
489
  if (!isObject(value))