evolcore 0.0.32 → 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 (150) hide show
  1. package/CHANGELOG.md +24 -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 +23 -19
  7. package/dist/agents/ecagent-runner.js +5 -13
  8. package/dist/aun/group/operations.js +0 -1
  9. package/dist/aun/msg/history.js +1 -0
  10. package/dist/aun/msg/p2p.js +0 -1
  11. package/dist/aun/outbox.js +50 -1
  12. package/dist/channels/aun.js +28 -2
  13. package/dist/channels/daemon.js +257 -2
  14. package/dist/cli/agent-command.js +58 -24
  15. package/dist/cli/agent.js +1 -1
  16. package/dist/cli/aun-commands.js +365 -29
  17. package/dist/cli/contact.js +8 -2
  18. package/dist/cli/daemon-commands.js +81 -12
  19. package/dist/cli/debug-mode.js +154 -0
  20. package/dist/cli/handoff-command.js +3 -0
  21. package/dist/cli/index.js +81 -64
  22. package/dist/cli/init.js +2 -2
  23. package/dist/cli/managed-command-guard.js +5 -0
  24. package/dist/cli/memory-command.js +8 -1
  25. package/dist/cli/queue-command.js +9 -4
  26. package/dist/cli/session-command.js +194 -0
  27. package/dist/cli/skill-command.js +78 -2
  28. package/dist/cli/task-context.js +1 -0
  29. package/dist/cli/thread-command.js +620 -0
  30. package/dist/cli/trigger-command.js +4 -2
  31. package/dist/config/builtin-roles.js +1 -0
  32. package/dist/config/config-field-policy.js +2 -3
  33. package/dist/config/config-manager.js +7 -2
  34. package/dist/config/resolved-config-op.js +3 -0
  35. package/dist/config-store.js +15 -0
  36. package/dist/core/auth/agent-delegation.js +25 -1
  37. package/dist/core/auth/debug-context.js +139 -0
  38. package/dist/core/auth/operation-authorizer.js +24 -1
  39. package/dist/core/auth/operation-catalog.js +33 -0
  40. package/dist/core/background-session/session-cleanup.js +212 -0
  41. package/dist/core/background-session/types.js +10 -0
  42. package/dist/core/bootstrap-messages.js +1 -1
  43. package/dist/core/bootstrap-service.js +6 -3
  44. package/dist/core/command/command-handler.js +60 -24
  45. package/dist/core/command/memory-slash.js +6 -2
  46. package/dist/core/command/menu-catalog.js +48 -1
  47. package/dist/core/command/menu-handler.js +92 -16
  48. package/dist/core/command/menu-protocol.js +24 -1
  49. package/dist/core/command/skill-slash.js +322 -0
  50. package/dist/core/command/slash-gate.js +6 -4
  51. package/dist/core/command/slash-handler.js +193 -13
  52. package/dist/core/command/topic-slash.js +528 -0
  53. package/dist/core/evolagent-registry.js +20 -5
  54. package/dist/core/handoff/runtime.js +5 -11
  55. package/dist/core/handoff/store.js +5 -0
  56. package/dist/core/ipc-registration.js +139 -2
  57. package/dist/core/memory/consolidation-context.js +49 -16
  58. package/dist/core/memory/consolidation-ops-records.js +35 -0
  59. package/dist/core/memory/consolidation-queue-format.js +21 -0
  60. package/dist/core/memory/consolidation.js +174 -180
  61. package/dist/core/message/aun-ipc-sender.js +161 -6
  62. package/dist/core/message/im-renderer.js +149 -6
  63. package/dist/core/message/message-bridge.js +13 -3
  64. package/dist/core/message/message-log.js +1 -0
  65. package/dist/core/message/peer-mode.js +6 -1
  66. package/dist/core/message/response-engine.js +349 -67
  67. package/dist/core/message/subtask-tool-projection.js +140 -0
  68. package/dist/core/model/catalog-service.js +17 -12
  69. package/dist/core/permission/ec-command-parser.js +126 -7
  70. package/dist/core/session/session-idle-monitor.js +249 -22
  71. package/dist/core/session/session-manager.js +54 -9
  72. package/dist/core/skill/command-parser.js +13 -0
  73. package/dist/core/skill/index-generator.js +4 -1
  74. package/dist/core/skill/startup.js +41 -0
  75. package/dist/core/skill/switch-resolver.js +55 -29
  76. package/dist/core/thread/command-parser.js +125 -0
  77. package/dist/core/thread/index.js +1 -0
  78. package/dist/core/thread/ipc-operation.js +95 -0
  79. package/dist/core/thread/thread-context-storage.js +131 -0
  80. package/dist/core/thread/thread-operation.js +258 -0
  81. package/dist/core/thread/thread-route.js +33 -0
  82. package/dist/core/thread/thread-service.js +777 -0
  83. package/dist/core/thread/types.js +1 -0
  84. package/dist/eck/message-renderer.js +35 -0
  85. package/dist/index.js +8 -5
  86. package/dist/ipc.js +89 -10
  87. package/dist/paths.js +23 -0
  88. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  89. package/dist/trigger/authorization.js +57 -28
  90. package/dist/trigger/feedback.js +7 -1
  91. package/dist/trigger/parser.js +63 -1
  92. package/dist/trigger/validation.js +14 -0
  93. package/dist/utils/error-utils.js +22 -1
  94. package/kits/docs/context-assembly.md +1 -0
  95. package/kits/docs/evolcore/INDEX.md +1 -0
  96. package/kits/docs/evolcore/agent-create.md +39 -28
  97. package/kits/docs/evolcore/agent.md +19 -4
  98. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  99. package/kits/docs/evolcore/group-collaboration.md +3 -5
  100. package/kits/docs/evolcore/group.md +3 -4
  101. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  102. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  103. package/kits/docs/evolcore/menu-protocol.md +7 -0
  104. package/kits/docs/evolcore/msg.md +5 -8
  105. package/kits/docs/evolcore/thread-chain.md +267 -0
  106. package/kits/docs/prompt-loading-architecture.md +1 -0
  107. package/kits/eck_manifest.json +61 -26
  108. package/kits/eck_message_manifest.json +169 -10
  109. package/kits/rules/05-venue.md +1 -1
  110. package/kits/rules/07-memory-and-thread.md +202 -0
  111. package/kits/schemas/relation-config.schema.9.json +18 -3
  112. package/kits/schemas/role-config.schema.1.json +6 -6
  113. package/kits/schemas/single-session.schema.1.json +6 -6
  114. package/kits/schemas/single-session.schema.2.json +6 -6
  115. package/kits/schemas/single-session.schema.3.json +6 -6
  116. package/kits/skills/.switches.json +12 -1
  117. package/kits/skills/ec-group/SKILL.md +234 -0
  118. package/kits/skills/ec-skill/SKILL.md +10 -1
  119. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  120. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  121. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  122. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  123. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  124. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  125. package/kits/skills/thread-ops/SKILL.md +492 -0
  126. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  127. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  128. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  129. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  130. package/kits/templates/bootstrap-welcome.md +7 -7
  131. package/kits/templates/memory-consolidation/forget.md +5 -0
  132. package/kits/templates/memory-consolidation/generalize.md +5 -0
  133. package/kits/templates/memory-consolidation/profile.md +5 -0
  134. package/kits/templates/memory-consolidation/promote.md +5 -0
  135. package/kits/templates/message-fragments/item.md +2 -1
  136. package/kits/templates/message-fragments/thread-context.md +6 -0
  137. package/kits/templates/message-fragments/trigger-session.md +2 -0
  138. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  139. package/kits/templates/roles/member.json +2 -0
  140. package/kits/templates/roles/visitor.json +2 -0
  141. package/kits/templates/system-fragments/bootstrap.md +66 -72
  142. package/kits/templates/system-fragments/channel.md +3 -6
  143. package/kits/templates/system-fragments/commands.md +1 -0
  144. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  145. package/kits/templates/system-fragments/reply.md +3 -2
  146. package/kits/templates/system-fragments/session.md +5 -14
  147. package/package.json +3 -1
  148. package/kits/docs/evolcore/group-fs.md +0 -17
  149. package/kits/docs/venues/aun-group.md +0 -45
  150. package/kits/docs/venues/group.md +0 -43
@@ -1,4 +1,4 @@
1
- import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable, TRANSPORT_EFFORTS, resolveCatalogModel, } from '../core/model/model-catalog.js';
1
+ import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable, } from '../core/model/model-catalog.js';
2
2
  // Ecagent/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
3
  import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
4
4
  import fs from 'fs';
@@ -762,15 +762,7 @@ export class EcagentRunner {
762
762
  return catalog.models.map(m => m.id);
763
763
  }
764
764
  async getAvailableEfforts(model) {
765
- const catalog = await this.getModelCatalog();
766
- const efforts = catalogEfforts(catalog, model, 'ecagent');
767
- if (efforts.length > 0)
768
- return efforts;
769
- // The embedded transport advertises reasoning support for every model it
770
- // constructs. Older gateways may omit reasoningLevels from /models; keep
771
- // menu.options aligned with the actual runner capability in that case.
772
- const entry = catalog.models.find(item => item.id === resolveCatalogModel(catalog, model));
773
- return entry && entry.reasoningLevels === undefined ? [...TRANSPORT_EFFORTS.ecagent] : efforts;
765
+ return catalogEfforts(await this.getModelCatalog(), model, 'ecagent');
774
766
  }
775
767
  registerStream(key, stream) { this.activeStreams.set(key, stream); }
776
768
  cleanupStream(key) {
@@ -917,7 +909,7 @@ export class EcagentRunner {
917
909
  if (pauseResult === 'cancelled')
918
910
  return { block: true, reason: '工具调用已取消' };
919
911
  }
920
- const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
912
+ const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input, stats.numTurns + 1);
921
913
  return lifecycle.cancelled || signal?.aborted ? { block: true, reason: '工具调用已取消' } : decision;
922
914
  });
923
915
  const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
@@ -1175,7 +1167,7 @@ export class EcagentRunner {
1175
1167
  model: model.id, effort,
1176
1168
  };
1177
1169
  }
1178
- async authorizeTool(sessionId, projectPath, mode, toolName, input) {
1170
+ async authorizeTool(sessionId, projectPath, mode, toolName, input, turn) {
1179
1171
  const permissionContext = this.permissionContexts.get(sessionId);
1180
1172
  if (mode === 'fullaccess') {
1181
1173
  return hasTrustedFullAccessContext(permissionContext)
@@ -1220,7 +1212,7 @@ export class EcagentRunner {
1220
1212
  logger.warn(`[EcagentRunner] failed to record blocked operation: session=${sessionId} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
1221
1213
  }
1222
1214
  };
1223
- const policy = permissionContext?.policyHook?.(toolName, input);
1215
+ const policy = permissionContext?.policyHook?.(toolName, input, { turn });
1224
1216
  if (policy?.block) {
1225
1217
  await recordBlockedOperation(policy.policyCode ?? 'session_policy_hook');
1226
1218
  return { block: true, reason: policy.reason || '当前会话策略拒绝此工具调用' };
@@ -117,7 +117,6 @@ async function tryDaemonGroupSend(args, payload, runtimeContext, file) {
117
117
  mentions: args.mentions,
118
118
  encrypt: args.encrypt,
119
119
  thread,
120
- returnPolicy: args.returnPolicy,
121
120
  originSessionId: runtimeContext.sessionId,
122
121
  originMessageId: runtimeContext.messageId,
123
122
  delegationToken: readDelegationCredential(),
@@ -171,6 +171,7 @@ function readHistoryScopes(input) {
171
171
  .filter(({ entry, sessionIds }) => sessionIds === undefined
172
172
  || (typeof entry.sessionId === 'string' && sessionIds.has(entry.sessionId)))
173
173
  .filter(({ entry }) => input.direction === 'all' || entry.dir === input.direction)
174
+ .filter(({ entry }) => input.args.threadId === undefined || entry.threadId === input.args.threadId)
174
175
  .filter(({ entry }) => input.args.after === undefined || entry.ts > input.args.after)
175
176
  .filter(({ entry }) => input.args.before === undefined || entry.ts < input.args.before)
176
177
  .sort((left, right) => left.entry.ts - right.entry.ts || left.scope - right.scope)
@@ -134,7 +134,6 @@ async function tryDaemonMsgSend(args, payload, runtimeContext, file) {
134
134
  ...(payload ? { payload } : { file }),
135
135
  encrypt: args.encrypt,
136
136
  thread,
137
- returnPolicy: args.returnPolicy,
138
137
  handoffContext: args.handoffContext,
139
138
  originSessionId: runtimeContext.sessionId,
140
139
  originMessageId: runtimeContext.messageId,
@@ -64,6 +64,24 @@ function isExpired(entry) {
64
64
  return false;
65
65
  return Date.now() - entry.ts > entry.ttl;
66
66
  }
67
+ /**
68
+ * A process-scoped entry whose enqueuing process is gone. Replaying it would
69
+ * deliver stale live feedback — typically the "restarting, back in ~5s" notice
70
+ * that a successful restart has already superseded with its own confirmation.
71
+ *
72
+ * A stale entry is not `isExpired()`: it needs `drainOnce` to retire it with a
73
+ * reason of its own, otherwise the send log would report a semantic drop as a
74
+ * timeout.
75
+ */
76
+ export function isStaleProcessScoped(entry) {
77
+ if (!entry.processScoped)
78
+ return false;
79
+ // A record without a pid cannot prove same-process ownership. Treat it as
80
+ // replayable so the ordinary route/expiry rules stay in charge.
81
+ if (typeof entry.enqueuedByPid !== 'number')
82
+ return false;
83
+ return entry.enqueuedByPid !== process.pid;
84
+ }
67
85
  export function isDeliveryForChannel(value, channelId) {
68
86
  return isDeliveryTargetForChannel(value, channelId);
69
87
  }
@@ -129,6 +147,7 @@ export function enqueue(aid, opts) {
129
147
  delivery,
130
148
  dedupeKey: opts.dedupeKey,
131
149
  critical: opts.critical,
150
+ ...(opts.processScoped ? { processScoped: true, enqueuedByPid: process.pid } : {}),
132
151
  type: opts.type,
133
152
  contentKind: opts.contentKind,
134
153
  payload: opts.payload,
@@ -273,6 +292,19 @@ function markExpired(entry) {
273
292
  code: entry.lastErrorCode,
274
293
  };
275
294
  }
295
+ /**
296
+ * Drop a process-scoped entry left behind by a previous process. The terminal
297
+ * record is retained so the send log still shows why it never went out.
298
+ */
299
+ function markSupersededProcessScoped(entry) {
300
+ entry.lastError = 'process-scoped delivery superseded by a new daemon process';
301
+ entry.lastErrorCode = 'OUTBOX_PROCESS_SUPERSEDED';
302
+ entry.terminal = {
303
+ at: Date.now(),
304
+ error: entry.lastError,
305
+ code: entry.lastErrorCode,
306
+ };
307
+ }
276
308
  /** Mark a durable entry terminal while keeping its reason for diagnostics. */
277
309
  export function markTerminal(aid, id, failure, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
278
310
  const entries = readEntries(aid, queue);
@@ -336,6 +368,7 @@ async function drainOnce(aid, sender, queue) {
336
368
  let expired = 0;
337
369
  let failed = 0;
338
370
  let permanent = 0;
371
+ let superseded = 0;
339
372
  const remaining = [];
340
373
  for (const entry of entries) {
341
374
  // Cancellation or another sender may have changed an
@@ -350,6 +383,16 @@ async function drainOnce(aid, sender, queue) {
350
383
  }
351
384
  Object.assign(entry, currentEntry);
352
385
  }
386
+ if (isStaleProcessScoped(entry)) {
387
+ // Deliberately not counted as `expired`: this is a semantic drop, not a
388
+ // timeout, and the drained-line diagnostic should not conflate them.
389
+ // The terminal record also retires the entry from `hasPending()`, so a
390
+ // later drain does not rediscover it.
391
+ superseded++;
392
+ markSupersededProcessScoped(entry);
393
+ remaining.push(entry);
394
+ continue;
395
+ }
353
396
  if (isExpired(entry)) {
354
397
  expired++;
355
398
  markExpired(entry);
@@ -457,7 +500,13 @@ async function drainOnce(aid, sender, queue) {
457
500
  }];
458
501
  });
459
502
  writeEntries(aid, retained, queue);
460
- return { sent, expired, failed, ...(permanent > 0 ? { permanent } : {}) };
503
+ return {
504
+ sent,
505
+ expired,
506
+ failed,
507
+ ...(permanent > 0 ? { permanent } : {}),
508
+ ...(superseded > 0 ? { superseded } : {}),
509
+ };
461
510
  }
462
511
  export async function drain(aid, sender, queue = 'default') {
463
512
  const drainKey = `${queue}:${aid}`;
@@ -4413,6 +4413,9 @@ export class AUNChannel {
4413
4413
  };
4414
4414
  if (evolCommand)
4415
4415
  base.ext = { evolCommand };
4416
+ // Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
4417
+ if (context?.debug)
4418
+ base.debug = { ...context.debug, sentAt: localTimestamp() };
4416
4419
  return withAunAttention(base);
4417
4420
  }
4418
4421
  async sendReliableStructured(channelId, payload, context, logText) {
@@ -4496,6 +4499,9 @@ export class AUNChannel {
4496
4499
  delivery,
4497
4500
  dedupeKey: operationId,
4498
4501
  critical: routedContext.metadata?.criticalDelivery === true,
4502
+ // Live feedback about the running process (for example the pre-restart
4503
+ // notice) must never be replayed by the process that replaces it.
4504
+ processScoped: routedContext.metadata?.processScopedDelivery === true,
4499
4505
  type: 'text',
4500
4506
  text: finalText,
4501
4507
  context: routedContext,
@@ -4565,6 +4571,16 @@ export class AUNChannel {
4565
4571
  };
4566
4572
  }
4567
4573
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
4574
+ /**
4575
+ * Agent 经 CLI 委托发送(ec msg send / ec group send)时占用本轮序号,
4576
+ * 与回复路径共用 sentCount,保证同一轮内 seqInTurn 连续。
4577
+ */
4578
+ nextSeqInTurn(channelId, threadId) {
4579
+ const turnKey = `${channelId}\0${threadId ?? ''}`;
4580
+ const seqInTurn = (this.sentCount.get(turnKey) || 0) + 1;
4581
+ this.sentCount.set(turnKey, seqInTurn);
4582
+ return seqInTurn;
4583
+ }
4568
4584
  async sendDaemonMsg(args) {
4569
4585
  if (!args.to) {
4570
4586
  return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
@@ -5271,6 +5287,9 @@ export class AUNChannel {
5271
5287
  delete payloadWithChatmode.thread_id;
5272
5288
  if (context?.threadId && !payloadWithChatmode.thread_id)
5273
5289
  payloadWithChatmode.thread_id = context.threadId;
5290
+ // Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
5291
+ if (context?.debug)
5292
+ payloadWithChatmode.debug = { ...context.debug, sentAt: localTimestamp() };
5274
5293
  const params = {
5275
5294
  context: { type: 'task', id: taskId },
5276
5295
  payload: this.limitThoughtPayload(payloadWithChatmode),
@@ -5847,8 +5866,12 @@ export class AUNChannel {
5847
5866
  }
5848
5867
  return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
5849
5868
  }, queue);
5850
- if (result.sent > 0 || result.expired > 0 || result.permanent) {
5851
- logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
5869
+ if (result.sent > 0 || result.expired > 0 || result.permanent || result.superseded) {
5870
+ const superseded = result.superseded ? ` superseded=${result.superseded}` : '';
5871
+ logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}${superseded}`);
5872
+ }
5873
+ if (result.superseded) {
5874
+ logger.info(`${this.logPrefix()} ${queue} outbox: dropped ${result.superseded} process-scoped entr${result.superseded === 1 ? 'y' : 'ies'} left by a previous daemon process`);
5852
5875
  }
5853
5876
  };
5854
5877
  await drainQueue('default');
@@ -5962,6 +5985,9 @@ export class AUNChannel {
5962
5985
  statusPayload.chatmode = context.metadata.chatmode;
5963
5986
  if (context?.replyToMessageId)
5964
5987
  statusPayload.ref_message_id = context.replyToMessageId;
5988
+ // Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
5989
+ if (context?.debug)
5990
+ statusPayload.debug = { ...context.debug, sentAt: localTimestamp() };
5965
5991
  const notifyOptions = { ttlMs: 60_000 };
5966
5992
  if (delivery.chatType === 'group')
5967
5993
  notifyOptions.groupId = delivery.groupId;
@@ -1,3 +1,4 @@
1
+ import { appendExpireEntry, recordNextLaunchCleanup, cleanupPreviousByKind, runExpireCleanup, cleanupSessionRecord, } from '../core/background-session/session-cleanup.js';
1
2
  import { ExecutionTimeoutError } from '../utils/error-utils.js';
2
3
  import { normalizeExecutionMode } from '../trigger/validation.js';
3
4
  import { resolvePeerRoleDetail } from '../config/peer-role-resolver.js';
@@ -17,6 +18,7 @@ export class DaemonChannel {
17
18
  thread: true,
18
19
  };
19
20
  pending = new Map();
21
+ bgPending = new Map();
20
22
  watchdogMs;
21
23
  totalExecutionMs;
22
24
  constructor(sessionManager, messageQueue, opts = {}) {
@@ -26,6 +28,8 @@ export class DaemonChannel {
26
28
  this.totalExecutionMs = normalizeTotalExecutionMs(opts.totalExecutionMs);
27
29
  }
28
30
  async converse(prompt, ctx) {
31
+ // 启动前跑一次 expire 扫描(成本低,只读少量小文件)
32
+ void runExpireCleanup(this.sessionManager).catch(() => { });
29
33
  const runId = ctx.runId;
30
34
  const pendingId = ctx.attemptId ?? runId;
31
35
  const messageId = pendingId;
@@ -41,6 +45,11 @@ export class DaemonChannel {
41
45
  ? undefined
42
46
  : setTimeout(() => this.totalExecutionTimeout(pendingId, remainingExecutionMs), remainingExecutionMs);
43
47
  totalExecutionTimer?.unref?.();
48
+ // trigger 清理策略:once → expire(3天),其他 → expire(30天)
49
+ const isOnce = ctx.trigger.source?.type === 'once';
50
+ const triggerCleanupPolicy = isOnce
51
+ ? { mode: 'expire', ttlMs: 3 * 24 * 3600 * 1000 }
52
+ : { mode: 'expire', ttlMs: 30 * 24 * 3600 * 1000 };
44
53
  this.pending.set(pendingId, {
45
54
  resolve,
46
55
  startedAt,
@@ -52,6 +61,11 @@ export class DaemonChannel {
52
61
  abortSignal: ctx.signal,
53
62
  runId,
54
63
  acc: { text: '', files: [], anomalies: [], toolCallCount: 0, seenToolCallIds: new Set() },
64
+ cleanupPolicy: triggerCleanupPolicy,
65
+ channelId: ctx.session.channelId,
66
+ agentAid: ctx.trigger.agentAid,
67
+ kind: `trigger:${ctx.trigger.id}`,
68
+ sessionId: ctx.session.id,
55
69
  });
56
70
  });
57
71
  const message = {
@@ -85,8 +99,13 @@ export class DaemonChannel {
85
99
  originChannelKey: ctx.trigger.origin?.channelKey,
86
100
  originChannelType: ctx.trigger.origin?.channelType
87
101
  ?? (ctx.trigger.origin?.channelKey ? channelTypeFromKey(ctx.trigger.origin.channelKey) : undefined),
102
+ originChatType: ctx.trigger.origin?.delivery?.chatType,
103
+ originConversationId: ctx.trigger.origin?.delivery?.chatType === 'group'
104
+ ? ctx.trigger.origin.channelId
105
+ : ctx.trigger.origin?.peerId,
88
106
  originControl: ctx.trigger.origin?.channelKey === 'daemon',
89
107
  authorizedBy: ctx.trigger.authorizedBy,
108
+ labels: ctx.trigger.labels,
90
109
  },
91
110
  };
92
111
  const onAbort = () => {
@@ -128,6 +147,15 @@ export class DaemonChannel {
128
147
  return promise;
129
148
  }
130
149
  async send(envelope, payload) {
150
+ // bg-session 路径:优先查 bgPending
151
+ const bgRunId = this.bgRunIdFromEnvelope(envelope);
152
+ if (bgRunId) {
153
+ const bgSlot = this.bgPending.get(bgRunId);
154
+ if (bgSlot && !bgSlot.terminated) {
155
+ this.handleBgSessionPayload(bgRunId, bgSlot, payload);
156
+ return;
157
+ }
158
+ }
131
159
  const runId = this.runIdFromEnvelope(envelope);
132
160
  if (!runId)
133
161
  return;
@@ -270,6 +298,11 @@ export class DaemonChannel {
270
298
  const pendingId = metadata?.triggerAttemptId ?? metadata?.triggerRunId;
271
299
  return typeof pendingId === 'string' ? pendingId : undefined;
272
300
  }
301
+ bgRunIdFromEnvelope(envelope) {
302
+ const metadata = envelope.replyContext?.metadata;
303
+ const runId = metadata?.bgSessionRunId;
304
+ return typeof runId === 'string' ? runId : undefined;
305
+ }
273
306
  recordToolCall(slot, callId) {
274
307
  if (typeof callId !== 'string' || !callId) {
275
308
  slot.acc.toolCallCount += 1;
@@ -290,13 +323,14 @@ export class DaemonChannel {
290
323
  ?? (origin.channelKey ? channelTypeFromKey(origin.channelKey) : undefined);
291
324
  if (!channelType)
292
325
  return undefined;
326
+ const chatType = origin.delivery?.chatType ?? 'private';
293
327
  return resolvePeerRoleDetail({
294
328
  selfAid: trigger.agentAid,
295
329
  channelKey: origin.channelKey,
296
330
  channelType,
297
- chatType: 'private',
331
+ chatType,
298
332
  actorId: origin.peerId,
299
- conversationId: origin.peerId,
333
+ conversationId: chatType === 'group' ? origin.channelId : origin.peerId,
300
334
  }).effectiveRole ?? undefined;
301
335
  }
302
336
  finish(runId, status, metadata) {
@@ -346,6 +380,29 @@ export class DaemonChannel {
346
380
  toolCallCount: Math.max(slot.acc.toolCallCount, completedToolCallCount),
347
381
  },
348
382
  });
383
+ // trigger 清理策略
384
+ const policy = slot.cleanupPolicy;
385
+ if (policy.mode === 'immediate') {
386
+ void cleanupSessionRecord({ sessionId: slot.sessionId, channelId: slot.channelId, agentAid: slot.agentAid }, this.sessionManager);
387
+ }
388
+ else if (policy.mode === 'on_next_launch') {
389
+ recordNextLaunchCleanup(slot.kind, {
390
+ channelId: slot.channelId,
391
+ sessionId: slot.sessionId,
392
+ agentAid: slot.agentAid,
393
+ recordedAt: Date.now(),
394
+ });
395
+ }
396
+ else if (policy.mode === 'expire') {
397
+ appendExpireEntry({
398
+ expireAt: Date.now() + policy.ttlMs,
399
+ sessionId: slot.sessionId,
400
+ channelId: slot.channelId,
401
+ kind: slot.kind,
402
+ agentAid: slot.agentAid,
403
+ });
404
+ }
405
+ // 'keep' 模式:不做任何处理
349
406
  }
350
407
  fail(runId, reason, text) {
351
408
  const slot = this.pending.get(runId);
@@ -412,6 +469,204 @@ export class DaemonChannel {
412
469
  }
413
470
  queue.cancelActive?.(messageId);
414
471
  }
472
+ // ── bg-session 生命周期 ──
473
+ handleBgSessionPayload(runId, slot, payload) {
474
+ if (slot.terminated)
475
+ return;
476
+ switch (payload.kind) {
477
+ case 'result.text':
478
+ if (payload.isFinal)
479
+ slot.text = payload.text;
480
+ else
481
+ slot.text += payload.text;
482
+ break;
483
+ case 'status.completed':
484
+ this.finishBgSession(runId, slot, 'completed');
485
+ break;
486
+ case 'result.error':
487
+ this.finishBgSession(runId, slot, 'error', payload.text ?? payload.reason ?? 'unknown error');
488
+ break;
489
+ case 'system.error':
490
+ this.finishBgSession(runId, slot, 'error', payload.text ?? payload.subtype ?? 'unknown error');
491
+ break;
492
+ case 'status.error':
493
+ this.finishBgSession(runId, slot, 'error', payload.metadata?.message ?? 'status error');
494
+ break;
495
+ case 'status.interrupted':
496
+ {
497
+ const reason = typeof payload.metadata?.reason === 'string'
498
+ ? payload.metadata.reason
499
+ : 'interrupted';
500
+ const outcome = slot.abortSignal?.aborted ? 'cancelled' : 'interrupted';
501
+ this.finishBgSession(runId, slot, outcome, reason);
502
+ }
503
+ break;
504
+ case 'status.timeout':
505
+ this.finishBgSession(runId, slot, 'error', 'bg-session watchdog timeout');
506
+ break;
507
+ default:
508
+ break;
509
+ }
510
+ }
511
+ finishBgSession(runId, slot, outcome, error) {
512
+ if (slot.terminated)
513
+ return;
514
+ slot.terminated = true;
515
+ if (slot.watchdogTimer)
516
+ clearTimeout(slot.watchdogTimer);
517
+ if (slot.abortListener)
518
+ slot.abortSignal?.removeEventListener('abort', slot.abortListener);
519
+ this.bgPending.delete(runId);
520
+ // 会话结束后按清理策略处理
521
+ const policy = slot.cleanupPolicy;
522
+ if (policy.mode === 'immediate') {
523
+ void cleanupSessionRecord({ sessionId: slot.sessionId, channelId: slot.channelId, agentAid: slot.agentAid }, this.sessionManager);
524
+ }
525
+ else if (policy.mode === 'on_next_launch') {
526
+ recordNextLaunchCleanup(slot.kind, {
527
+ channelId: slot.channelId,
528
+ sessionId: slot.sessionId,
529
+ agentAid: slot.agentAid,
530
+ recordedAt: Date.now(),
531
+ });
532
+ }
533
+ else if (policy.mode === 'expire') {
534
+ appendExpireEntry({
535
+ expireAt: Date.now() + policy.ttlMs,
536
+ sessionId: slot.sessionId,
537
+ channelId: slot.channelId,
538
+ kind: slot.kind,
539
+ agentAid: slot.agentAid,
540
+ });
541
+ }
542
+ // 'keep' 模式:不做任何处理
543
+ slot.resolve({
544
+ sessionId: slot.sessionId,
545
+ outcome,
546
+ ...(slot.text ? { text: slot.text } : {}),
547
+ ...(error ? { error } : {}),
548
+ });
549
+ }
550
+ /**
551
+ * 拉起一个独立后台会话。会话创建、首条消息入队后即返回 handle;
552
+ * 会话结束由 handle.result 通知。实现 BackgroundSessionLauncher 接口。
553
+ * 详见 docs/background-session/background-session-launcher.md
554
+ */
555
+ async launchBackgroundSession(spec) {
556
+ const runId = `bg-${spec.kind}-${spec.instanceId}`;
557
+ const channelId = `bg:${spec.kind}:${spec.instanceId}`;
558
+ const cleanupPolicy = spec.cleanupPolicy ?? { mode: 'on_next_launch' };
559
+ // 启动前清理:on_next_launch 清理同 kind 历史,expire 扫描到期条目
560
+ if (cleanupPolicy.mode === 'on_next_launch') {
561
+ await cleanupPreviousByKind(spec.kind, this.sessionManager);
562
+ }
563
+ else if (cleanupPolicy.mode !== 'keep') {
564
+ // immediate 和 expire 模式都在启动时跑一次 expire 扫描
565
+ await runExpireCleanup(this.sessionManager);
566
+ }
567
+ const session = await this.sessionManager.getOrCreateSession(this.channelName, channelId, spec.projectPath, channelId, // threadId,每次独立,保证 by_run 语义
568
+ {
569
+ channelKey: this.channelKey,
570
+ peerId: `bg:${spec.kind}`,
571
+ peerName: spec.kind,
572
+ peerType: 'system',
573
+ bgKind: spec.kind,
574
+ bgInstanceId: spec.instanceId,
575
+ ...(spec.metadata ?? {}),
576
+ }, spec.kind, undefined, 'private', spec.baseagent, spec.agentAid, this.channelName, 'system');
577
+ const promise = new Promise((resolve) => {
578
+ const slot = {
579
+ resolve,
580
+ sessionId: session.id,
581
+ channelId,
582
+ kind: spec.kind,
583
+ startedAt: Date.now(),
584
+ abortSignal: spec.signal,
585
+ text: '',
586
+ terminated: false,
587
+ cleanupPolicy,
588
+ agentAid: spec.agentAid,
589
+ watchdogTimer: setTimeout(() => {
590
+ this.finishBgSession(runId, slot, 'error', 'bg-session watchdog timeout');
591
+ }, this.watchdogMs),
592
+ };
593
+ slot.watchdogTimer?.unref?.();
594
+ if (spec.signal) {
595
+ const onAbort = () => {
596
+ void this.cancelDelivery(runId).catch(() => { });
597
+ };
598
+ slot.abortListener = onAbort;
599
+ spec.signal.addEventListener('abort', onAbort, { once: true });
600
+ if (spec.signal.aborted)
601
+ onAbort();
602
+ }
603
+ this.bgPending.set(runId, slot);
604
+ });
605
+ const message = {
606
+ channel: this.channelName,
607
+ channelType: this.channelName,
608
+ channelId: session.channelId,
609
+ selfAID: spec.agentAid,
610
+ baseagent: spec.baseagent,
611
+ threadId: session.threadId || '',
612
+ chatType: 'private',
613
+ peerId: `bg:${spec.kind}`,
614
+ peerName: spec.kind,
615
+ peerType: 'system',
616
+ content: spec.prompt,
617
+ messageId: runId,
618
+ timestamp: Date.now(),
619
+ source: 'bg-session',
620
+ bgSessionMeta: {
621
+ kind: spec.kind,
622
+ runId,
623
+ instanceId: spec.instanceId,
624
+ boundSessionId: session.id,
625
+ },
626
+ replyContext: {
627
+ metadata: { bgSessionRunId: runId },
628
+ },
629
+ };
630
+ void this.messageQueue.enqueue?.(session.id, message, spec.projectPath, {
631
+ interruptible: false,
632
+ agentName: spec.agentAid,
633
+ sessionKeyField: session.sessionKey,
634
+ selfAID: spec.agentAid,
635
+ });
636
+ let settled = false;
637
+ void promise.then(() => { settled = true; });
638
+ const sessionId = session.id;
639
+ return {
640
+ sessionId,
641
+ result: promise,
642
+ waitForAgentSessionId: (opts) => this.waitForAgentSessionId(sessionId, () => settled, opts),
643
+ };
644
+ }
645
+ /**
646
+ * 轮询 session 记录,等 runner 写入 agentSessionId。
647
+ * 会话刚创建时只有 evolcore sessionId,agentSessionId 要等 runner 初始化并开始第一轮才写入。
648
+ * 会话结束后再读一次即停止,避免对已结束的会话空等到超时。
649
+ */
650
+ async waitForAgentSessionId(sessionId, isSettled, opts = {}) {
651
+ const deadline = Date.now() + (opts.timeoutMs ?? 10_000);
652
+ const interval = opts.intervalMs ?? 250;
653
+ while (true) {
654
+ const settledBeforeRead = isSettled();
655
+ try {
656
+ const id = (await this.sessionManager.getSessionById(sessionId))?.agentSessionId;
657
+ if (id)
658
+ return id;
659
+ }
660
+ catch { /* 会话记录写到一半,下一轮再读 */ }
661
+ if (settledBeforeRead || Date.now() >= deadline)
662
+ return undefined;
663
+ await new Promise(r => setTimeout(r, interval));
664
+ }
665
+ }
666
+ /** 实现 BackgroundSessionLauncher 接口 */
667
+ launch(spec) {
668
+ return this.launchBackgroundSession(spec);
669
+ }
415
670
  }
416
671
  function isObject(value) {
417
672
  return !!value && typeof value === 'object' && !Array.isArray(value);