evolcore 0.0.27 → 0.0.29-test.0

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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -2,14 +2,228 @@ import { ensureDir } from '../../utils/atomic-write.js';
2
2
  import { resolvePaths } from '../../paths.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { encodePath } from '../../utils/cross-platform.js';
5
- import { readRunnerTaskHistory } from './runner-task-history.js';
5
+ import { readRunnerEventSummary } from './runner-event-summary.js';
6
+ import { isInternalInjectedUserMessage, isToolResultMessage } from './session-transcript.js';
6
7
  import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
7
- import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, isArchivedTopicLable, normalizeTopicLable, } from './session-mapper.js';
8
+ import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, normalizeTopicLabel, normalizeTopicStatus, } from './session-mapper.js';
8
9
  import { tryParseChannelKey } from '../channel-loader.js';
9
10
  import { isSystemControlChannel } from '../system-channels.js';
10
11
  import path from 'path';
11
12
  import fs from 'fs';
12
13
  import os from 'os';
14
+ import { createHash } from 'node:crypto';
15
+ function forkHistoryRecord(value) {
16
+ return value && typeof value === 'object' && !Array.isArray(value)
17
+ ? value
18
+ : undefined;
19
+ }
20
+ export function isForkHistorySnapshot(value) {
21
+ const snapshot = forkHistoryRecord(value);
22
+ if (snapshot?.version !== 1 || typeof snapshot.assistantMessageId !== 'string'
23
+ || !snapshot.assistantMessageId || !Array.isArray(snapshot.items))
24
+ return false;
25
+ return snapshot.items.every((value, index) => {
26
+ const item = forkHistoryRecord(value);
27
+ return item?.index === index
28
+ && typeof item.sourceMessageId === 'string'
29
+ && (item.role === 'user' || item.role === 'assistant')
30
+ && ['text', 'tool_call', 'tool_result', 'attachment', 'file_change'].includes(String(item.kind))
31
+ && typeof item.text === 'string'
32
+ && (item.tool === undefined || forkHistoryRecord(item.tool) !== undefined);
33
+ });
34
+ }
35
+ const MAX_FORK_HISTORY_ITEM_TEXT_BYTES = 16 * 1024;
36
+ const MAX_FORK_HISTORY_TOOL_VALUE_BYTES = 8 * 1024;
37
+ const MAX_FORK_HISTORY_PAGE_JSON_BYTES = 128 * 1024;
38
+ function cleanForkHistoryUserText(text) {
39
+ let cleaned = text.replace(/^【新消息插入】\s*/, '')
40
+ .replace(/\s*【(?:请无视之前中断继续处理|请根据前后消息酌情处理)】$/, '').trim();
41
+ if (cleaned.startsWith('‹')) {
42
+ const newline = cleaned.indexOf('\n');
43
+ if (newline >= 0)
44
+ cleaned = cleaned.slice(newline + 1).trim();
45
+ }
46
+ if (cleaned.startsWith('<messages>')) {
47
+ const parts = [...cleaned.matchAll(/<message(?:\s[^>]*)?>([\s\S]*?)<\/message>/g)]
48
+ .map(match => match[1].trim());
49
+ if (parts.length > 0)
50
+ cleaned = parts.join('\n');
51
+ }
52
+ return cleaned;
53
+ }
54
+ function splitForkHistoryText(text) {
55
+ if (Buffer.byteLength(text, 'utf8') <= MAX_FORK_HISTORY_ITEM_TEXT_BYTES)
56
+ return [text];
57
+ const chunks = [];
58
+ let chunk = '';
59
+ let bytes = 0;
60
+ for (const character of text) {
61
+ const characterBytes = Buffer.byteLength(character, 'utf8');
62
+ if (bytes + characterBytes > MAX_FORK_HISTORY_ITEM_TEXT_BYTES && chunk) {
63
+ chunks.push(chunk);
64
+ chunk = '';
65
+ bytes = 0;
66
+ }
67
+ chunk += character;
68
+ bytes += characterBytes;
69
+ }
70
+ if (chunk)
71
+ chunks.push(chunk);
72
+ return chunks;
73
+ }
74
+ function projectForkHistoryToolValue(value) {
75
+ if (value === undefined)
76
+ return undefined;
77
+ try {
78
+ const serialized = JSON.stringify(value);
79
+ if (serialized === undefined)
80
+ return undefined;
81
+ if (Buffer.byteLength(serialized, 'utf8') <= MAX_FORK_HISTORY_TOOL_VALUE_BYTES) {
82
+ return JSON.parse(serialized);
83
+ }
84
+ let preview = '';
85
+ let bytes = 0;
86
+ for (const character of serialized) {
87
+ const characterBytes = Buffer.byteLength(character, 'utf8');
88
+ if (bytes + characterBytes > MAX_FORK_HISTORY_TOOL_VALUE_BYTES - 16)
89
+ break;
90
+ preview += character;
91
+ bytes += characterBytes;
92
+ }
93
+ return `${preview}... [truncated]`;
94
+ }
95
+ catch {
96
+ return '[unavailable]';
97
+ }
98
+ }
99
+ function projectForkHistoryContent(content, type) {
100
+ if (typeof content === 'string') {
101
+ const text = type === 'user' ? cleanForkHistoryUserText(content) : content.trim();
102
+ return text ? [{ kind: 'text', text }] : [];
103
+ }
104
+ if (!Array.isArray(content))
105
+ return [];
106
+ const parts = [];
107
+ for (const rawBlock of content) {
108
+ const block = forkHistoryRecord(rawBlock);
109
+ if (!block)
110
+ continue;
111
+ const blockType = typeof block.type === 'string' ? block.type : '';
112
+ if (blockType === 'text' || blockType === 'input_text' || blockType === 'output_text') {
113
+ const text = typeof block.text === 'string'
114
+ ? (type === 'user' ? cleanForkHistoryUserText(block.text) : block.text.trim())
115
+ : '';
116
+ if (text)
117
+ parts.push({ kind: 'text', text });
118
+ }
119
+ else if (blockType === 'tool_use') {
120
+ const name = typeof block.name === 'string' ? block.name : '工具';
121
+ parts.push({
122
+ kind: 'tool_call',
123
+ text: name,
124
+ tool: {
125
+ name,
126
+ ...(typeof block.id === 'string' ? { callId: block.id } : {}),
127
+ ...(block.input !== undefined ? { arguments: projectForkHistoryToolValue(block.input) } : {}),
128
+ },
129
+ });
130
+ }
131
+ else if (blockType === 'tool_result') {
132
+ parts.push({
133
+ kind: 'tool_result',
134
+ text: '工具返回结果',
135
+ tool: {
136
+ ...(typeof block.name === 'string' ? { name: block.name } : {}),
137
+ ...(typeof block.tool_use_id === 'string' ? { callId: block.tool_use_id } : {}),
138
+ ...(block.content !== undefined ? { result: projectForkHistoryToolValue(block.content) } : {}),
139
+ ok: block.is_error !== true,
140
+ },
141
+ });
142
+ }
143
+ else if (blockType === 'image' || blockType === 'localImage' || blockType === 'image_url') {
144
+ parts.push({ kind: 'attachment', text: '[图片]' });
145
+ }
146
+ else if (blockType === 'document' || blockType === 'file') {
147
+ const name = typeof block.name === 'string' ? block.name : '';
148
+ parts.push({ kind: 'attachment', text: name ? `[文件] ${name}` : '[文件]' });
149
+ }
150
+ else if (blockType === 'file_change') {
151
+ parts.push({ kind: 'file_change', text: '文件变更' });
152
+ }
153
+ }
154
+ return parts;
155
+ }
156
+ export function buildForkHistorySnapshot(messages, assistantMessageId) {
157
+ let boundary = -1;
158
+ for (let index = 0; index < messages.length; index++) {
159
+ if (messages[index].uuid === assistantMessageId
160
+ && (messages[index].type === 'assistant'
161
+ || (messages[index].type === 'user'
162
+ && !isInternalInjectedUserMessage(messages[index].message)))) {
163
+ if (boundary >= 0)
164
+ throw new Error('fork history anchor is ambiguous');
165
+ boundary = index;
166
+ }
167
+ }
168
+ if (boundary < 0)
169
+ throw new Error('fork history anchor is missing');
170
+ const items = [];
171
+ for (let sourceIndex = 0; sourceIndex <= boundary; sourceIndex++) {
172
+ const source = messages[sourceIndex];
173
+ if (source.type === 'user' && isInternalInjectedUserMessage(source.message))
174
+ continue;
175
+ if (source.type !== 'user' && source.type !== 'assistant' && source.type !== 'system')
176
+ continue;
177
+ const message = forkHistoryRecord(source.message);
178
+ const content = message?.content ?? source.message;
179
+ const toolResult = source.type === 'user' && isToolResultMessage(source.message);
180
+ const role = source.type === 'user' && !toolResult ? 'user' : 'assistant';
181
+ const projected = projectForkHistoryContent(content, source.type);
182
+ for (let partIndex = 0; partIndex < projected.length; partIndex++) {
183
+ const part = projected[partIndex];
184
+ if (source.type === 'system' && part.kind !== 'file_change')
185
+ continue;
186
+ const chunks = splitForkHistoryText(part.text);
187
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
188
+ items.push({
189
+ index: items.length,
190
+ sourceMessageId: `${source.uuid || sourceIndex}:${sourceIndex}:${partIndex}:${chunkIndex}`,
191
+ role,
192
+ kind: toolResult ? 'tool_result' : part.kind,
193
+ text: chunks[chunkIndex],
194
+ ...(part.tool ? { tool: part.tool } : {}),
195
+ });
196
+ }
197
+ }
198
+ }
199
+ if (items.length === 0)
200
+ throw new Error('fork history has no displayable messages');
201
+ return { version: 1, assistantMessageId, items };
202
+ }
203
+ export function pageForkHistory(snapshot, before = snapshot.items.length, limit = 20) {
204
+ if (!Number.isInteger(before) || before < 1 || before > snapshot.items.length
205
+ || !Number.isInteger(limit) || limit < 1 || limit > 20) {
206
+ throw new Error('invalid fork history page');
207
+ }
208
+ let start = before;
209
+ let bytes = 2;
210
+ while (start > 0 && before - start < limit) {
211
+ const itemBytes = Buffer.byteLength(JSON.stringify(snapshot.items[start - 1]), 'utf8');
212
+ const nextBytes = bytes + itemBytes + (start < before ? 1 : 0);
213
+ if (nextBytes > MAX_FORK_HISTORY_PAGE_JSON_BYTES) {
214
+ if (start === before)
215
+ throw new Error('fork history item exceeds page budget');
216
+ break;
217
+ }
218
+ bytes = nextBytes;
219
+ start--;
220
+ }
221
+ return {
222
+ items: snapshot.items.slice(start, before),
223
+ nextBefore: start > 0 ? start : null,
224
+ total: snapshot.items.length,
225
+ };
226
+ }
13
227
  /** Metadata keys owned by ordinary topic/session routing writers. */
14
228
  const TOPIC_METADATA_PROTECTED_KEYS = new Set(['turnState', 'resumeAt', 'agentSessions']);
15
229
  const TOPIC_BACKEND_SCOPED_KEYS = ['resumeAt', 'agentSessions'];
@@ -292,6 +506,119 @@ export class SessionManager {
292
506
  const targetDir = session.threadId ? path.join(dir, '_threads') : dir;
293
507
  return this.metaFilePath(targetDir, session.id);
294
508
  }
509
+ forkHistoryPath(session) {
510
+ if (!session.threadId)
511
+ throw new Error('fork history requires a topic session');
512
+ return path.join(this.resolveChatDirFromSession(session), '_threads', `${session.id}.fork-history.json`);
513
+ }
514
+ forkHistoryBackupPath(session) {
515
+ return `${this.forkHistoryPath(session)}.bak`;
516
+ }
517
+ forkHistoryText(snapshot, assistantMessageId) {
518
+ if (!isForkHistorySnapshot(snapshot) || snapshot.items.length === 0
519
+ || snapshot.assistantMessageId !== assistantMessageId) {
520
+ throw new Error('invalid fork history snapshot');
521
+ }
522
+ return `${JSON.stringify(snapshot)}\n`;
523
+ }
524
+ forkHistoryDigest(content) {
525
+ return createHash('sha256').update(content, 'utf8').digest('hex');
526
+ }
527
+ readForkHistory(session) {
528
+ const source = session.metadata?.forkSource;
529
+ if (!source?.historyVersion)
530
+ return undefined;
531
+ if (source.historyVersion !== 1 || typeof source.historyDigest !== 'string'
532
+ || !/^[a-f0-9]{64}$/.test(source.historyDigest)
533
+ || !Number.isInteger(source.historyCount) || source.historyCount < 1) {
534
+ throw new Error('fork history metadata is invalid');
535
+ }
536
+ const primary = this.forkHistoryPath(session);
537
+ const backup = this.forkHistoryBackupPath(session);
538
+ const checkedRead = (file) => {
539
+ const content = fs.readFileSync(file, 'utf8');
540
+ if (this.forkHistoryDigest(content) !== source.historyDigest) {
541
+ throw new Error('fork history checksum mismatch');
542
+ }
543
+ const snapshot = JSON.parse(content);
544
+ if (!isForkHistorySnapshot(snapshot) || snapshot.assistantMessageId !== source.assistantMessageId
545
+ || snapshot.items.length !== source.historyCount) {
546
+ throw new Error('fork history snapshot is invalid');
547
+ }
548
+ return snapshot;
549
+ };
550
+ try {
551
+ return checkedRead(primary);
552
+ }
553
+ catch (primaryError) {
554
+ try {
555
+ const snapshot = checkedRead(backup);
556
+ try {
557
+ atomicWriteText(primary, fs.readFileSync(backup, 'utf8'));
558
+ }
559
+ catch (error) {
560
+ logger.warn(`[SessionManager] Failed to repair fork history primary: ${error}`);
561
+ }
562
+ return snapshot;
563
+ }
564
+ catch {
565
+ throw primaryError;
566
+ }
567
+ }
568
+ }
569
+ async attachForkHistory(sessionId, snapshot) {
570
+ const loaded = this.loadSessionForUpdate(sessionId);
571
+ const session = loaded?.current;
572
+ const source = session?.metadata?.forkSource;
573
+ if (!session?.threadId || !source)
574
+ throw new Error('fork topic is missing');
575
+ if (source.historyVersion) {
576
+ this.readForkHistory(session);
577
+ return session;
578
+ }
579
+ const file = this.forkHistoryPath(session);
580
+ const backup = this.forkHistoryBackupPath(session);
581
+ let content;
582
+ let validFile;
583
+ let recovered;
584
+ for (const candidate of [file, backup]) {
585
+ try {
586
+ const existingContent = fs.readFileSync(candidate, 'utf8');
587
+ const existing = JSON.parse(existingContent);
588
+ if (!isForkHistorySnapshot(existing) || existing.items.length === 0
589
+ || existing.assistantMessageId !== source.assistantMessageId)
590
+ continue;
591
+ content = existingContent;
592
+ recovered = existing;
593
+ validFile = candidate;
594
+ break;
595
+ }
596
+ catch {
597
+ // Try the other copy or a projected legacy transcript.
598
+ }
599
+ }
600
+ if (!content || !recovered) {
601
+ if (!snapshot)
602
+ return undefined;
603
+ content = this.forkHistoryText(snapshot, source.assistantMessageId);
604
+ recovered = snapshot;
605
+ }
606
+ if (validFile !== file)
607
+ atomicWriteText(file, content);
608
+ if (validFile !== backup)
609
+ atomicWriteText(backup, content);
610
+ session.metadata = {
611
+ ...session.metadata,
612
+ forkSource: {
613
+ ...source,
614
+ historyVersion: 1,
615
+ historyCount: recovered.items.length,
616
+ historyDigest: this.forkHistoryDigest(content),
617
+ },
618
+ };
619
+ this.persistSession(session, 'none');
620
+ return session;
621
+ }
295
622
  appendMeta(channel, channelId, session) {
296
623
  const file = sessionToFile(session);
297
624
  const metaPath = this.metaPathForSession(session);
@@ -332,9 +659,10 @@ export class SessionManager {
332
659
  // backend boundary itself. Always take the latest binding and owner.
333
660
  session.agentSessionId = latestId;
334
661
  session.baseagent = latest.baseagent;
335
- // Classification is owned by setTopicLable, not stale turn snapshots.
336
- if (!opts?.lableMutation)
337
- session.lable = latest.lable;
662
+ if (!opts?.topicMutation) {
663
+ session.label = latest.label;
664
+ session.topicStatus = latest.topicStatus;
665
+ }
338
666
  // Merge metadata at field level as well. A caller can hold a stale
339
667
  // Session object across an await; preserving keys from the latest
340
668
  // snapshot prevents unrelated turn/route updates from being erased.
@@ -349,6 +677,9 @@ export class SessionManager {
349
677
  : { turnState: undefined })
350
678
  : {}),
351
679
  };
680
+ if (latest.metadata?.forkSource?.historyVersion === 1) {
681
+ mergedMetadata.forkSource = JSON.parse(JSON.stringify(latest.metadata.forkSource));
682
+ }
352
683
  for (const key of TOPIC_BACKEND_SCOPED_KEYS) {
353
684
  if (Object.prototype.hasOwnProperty.call(latest.metadata ?? {}, key)) {
354
685
  const value = latest.metadata[key];
@@ -445,6 +776,12 @@ export class SessionManager {
445
776
  this.persistSession(session, 'sync');
446
777
  return undefined;
447
778
  }
779
+ hasBoundAgentSession(session) {
780
+ if (!session.agentSessionId)
781
+ return false;
782
+ const adapter = this.getFileAdapter(session.baseagent);
783
+ return !!adapter?.checkExists(session.projectPath, session.agentSessionId, session.selfAID);
784
+ }
448
785
  getActiveChatType(channel, channelId) {
449
786
  const active = this.readActive(channel, channelId);
450
787
  if (active && !active.threadId)
@@ -550,6 +887,69 @@ export class SessionManager {
550
887
  throw error;
551
888
  }
552
889
  }
890
+ assertTopicResumeMutationTarget(current, input) {
891
+ if (!current.threadId || current.threadId !== input.expectedThreadId) {
892
+ const error = new Error('topic resumeAt threadId mismatch');
893
+ error.code = 'THREAD_MISMATCH';
894
+ throw error;
895
+ }
896
+ if (input.expectedAgentSessionId !== undefined
897
+ && normalizeBackendSessionId(current.agentSessionId) !== normalizeBackendSessionId(input.expectedAgentSessionId)) {
898
+ const error = new Error('topic resumeAt backend mismatch');
899
+ error.code = 'BACKEND_MISMATCH';
900
+ throw error;
901
+ }
902
+ }
903
+ /** Persist a pending Claude-style topic rewind anchor without generic metadata writers. */
904
+ async setTopicResumeAt(input) {
905
+ const resumeAt = input.resumeAt.trim();
906
+ if (!input.expectedThreadId.trim())
907
+ throw new Error('topic resumeAt requires a non-empty threadId');
908
+ if (!resumeAt)
909
+ throw new Error('topic resumeAt requires a non-empty anchor');
910
+ this.mutateLatestTopicSession(input.sessionId, (current, found) => {
911
+ this.assertTopicResumeMutationTarget(current, input);
912
+ const next = JSON.parse(JSON.stringify(current));
913
+ next.metadata = { ...(next.metadata ?? {}), resumeAt };
914
+ this.persistConfirmedTopicBoundary(current, found, next);
915
+ });
916
+ }
917
+ /** Clear a pending topic rewind anchor after provider rollback or backend rotation. */
918
+ async clearTopicResumeAt(input) {
919
+ if (!input.expectedThreadId.trim())
920
+ throw new Error('topic resumeAt requires a non-empty threadId');
921
+ this.mutateLatestTopicSession(input.sessionId, (current, found) => {
922
+ this.assertTopicResumeMutationTarget(current, input);
923
+ if (!current.metadata || !Object.prototype.hasOwnProperty.call(current.metadata, 'resumeAt'))
924
+ return;
925
+ const next = JSON.parse(JSON.stringify(current));
926
+ const metadata = { ...(next.metadata ?? {}) };
927
+ delete metadata.resumeAt;
928
+ next.metadata = metadata;
929
+ this.persistConfirmedTopicBoundary(current, found, next);
930
+ });
931
+ }
932
+ /** Atomically consume a topic rewind anchor for the matching backend turn. */
933
+ async consumeTopicResumeAt(input) {
934
+ if (!input.expectedThreadId.trim())
935
+ throw new Error('topic resumeAt requires a non-empty threadId');
936
+ return this.mutateLatestTopicSession(input.sessionId, (current, found) => {
937
+ this.assertTopicResumeMutationTarget(current, input);
938
+ const resumeAt = typeof current.metadata?.resumeAt === 'string'
939
+ ? current.metadata.resumeAt.trim()
940
+ : '';
941
+ if (!resumeAt)
942
+ return undefined;
943
+ if (input.expectedResumeAt !== undefined && resumeAt !== input.expectedResumeAt.trim())
944
+ return undefined;
945
+ const next = JSON.parse(JSON.stringify(current));
946
+ const metadata = { ...(next.metadata ?? {}) };
947
+ delete metadata.resumeAt;
948
+ next.metadata = metadata;
949
+ this.persistConfirmedTopicBoundary(current, found, next);
950
+ return resumeAt;
951
+ });
952
+ }
553
953
  /** Clear a topic binding in one short latest-snapshot mutation. */
554
954
  async rotateTopicBackend(input) {
555
955
  if (!input.expectedThreadId.trim())
@@ -730,7 +1130,7 @@ export class SessionManager {
730
1130
  return result;
731
1131
  }
732
1132
  // ─── Session lifecycle ───
733
- async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, baseagent, selfAID, channelType, peerType, identity, lable) {
1133
+ async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, baseagent, selfAID, channelType, peerType, identity, label, topicStatus) {
734
1134
  if (isSystemControlChannel(channel) || isSystemControlChannel(channelType)) {
735
1135
  throw new Error(`[SessionManager] system control channel is sessionless. channel="${channel}"`);
736
1136
  }
@@ -744,7 +1144,7 @@ export class SessionManager {
744
1144
  // 用于 resolveIdentity fallback 正确命中群成员角色表(见 resolvePeerRoleDetail)。
745
1145
  const identityConversationId = chatType === 'group' ? (metadata?.groupId || channelId) : userId;
746
1146
  if (threadId) {
747
- const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, lable);
1147
+ const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus);
748
1148
  session.identity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
749
1149
  return session;
750
1150
  }
@@ -965,7 +1365,7 @@ export class SessionManager {
965
1365
  current.metadata = metadata;
966
1366
  this.persistSession(current, 'sync', { turnStateMutation: true });
967
1367
  }
968
- getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, lable) {
1368
+ getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus) {
969
1369
  // 使用精确路径(channelType + selfAID)
970
1370
  const chatDir = (channelType && selfAID)
971
1371
  ? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
@@ -1028,7 +1428,8 @@ export class SessionManager {
1028
1428
  chatType: effectiveChatType,
1029
1429
  metadata,
1030
1430
  name: name || '话题会话',
1031
- lable: normalizeTopicLable(lable),
1431
+ label: normalizeTopicLabel(label),
1432
+ topicStatus: topicStatus ? normalizeTopicStatus(topicStatus) : 'pending',
1032
1433
  createdAt: Date.now(),
1033
1434
  updatedAt: Date.now(),
1034
1435
  };
@@ -1307,7 +1708,7 @@ export class SessionManager {
1307
1708
  return true;
1308
1709
  if (!session.threadId)
1309
1710
  return archiveFilter !== 'archived';
1310
- const archived = isArchivedTopicLable(session.lable);
1711
+ const archived = session.topicStatus === 'archived';
1311
1712
  return archiveFilter === 'archived' ? archived : !archived;
1312
1713
  });
1313
1714
  return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
@@ -1376,13 +1777,22 @@ export class SessionManager {
1376
1777
  this.persistSession(current, 'sync');
1377
1778
  return true;
1378
1779
  }
1379
- async setTopicLable(sessionId, lable) {
1380
- const normalized = normalizeTopicLable(lable);
1780
+ async setTopicLabel(sessionId, label) {
1781
+ const normalized = normalizeTopicLabel(label);
1782
+ const loaded = this.loadSessionForUpdate(sessionId);
1783
+ if (!loaded?.current.threadId)
1784
+ return false;
1785
+ loaded.current.label = normalized;
1786
+ this.persistSession(loaded.current, 'sync', { topicMutation: true });
1787
+ return true;
1788
+ }
1789
+ async setTopicStatus(sessionId, status) {
1790
+ const normalized = normalizeTopicStatus(status);
1381
1791
  const loaded = this.loadSessionForUpdate(sessionId);
1382
1792
  if (!loaded?.current.threadId)
1383
1793
  return false;
1384
- loaded.current.lable = normalized;
1385
- this.persistSession(loaded.current, 'sync', { lableMutation: true });
1794
+ loaded.current.topicStatus = normalized;
1795
+ this.persistSession(loaded.current, 'sync', { topicMutation: true });
1386
1796
  return true;
1387
1797
  }
1388
1798
  async unbindSession(sessionId) {
@@ -1401,6 +1811,17 @@ export class SessionManager {
1401
1811
  }
1402
1812
  // If thread session, remove from thread-index
1403
1813
  if (found.isThread) {
1814
+ for (const suffix of ['.fork-history.json', '.fork-history.json.bak', '.fork-history.json.tmp']) {
1815
+ const historyFile = path.join(found.chatDir, '_threads', `${sessionId}${suffix}`);
1816
+ const historyTrash = path.join(trashDir, `${sessionId}${suffix}`);
1817
+ try {
1818
+ fs.renameSync(historyFile, historyTrash);
1819
+ }
1820
+ catch (e) {
1821
+ if (e.code !== 'ENOENT')
1822
+ logger.warn(`[SessionManager] Failed to move fork history to trash: ${e}`);
1823
+ }
1824
+ }
1404
1825
  const threadIndex = readThreadIndex(found.chatDir);
1405
1826
  for (const [tid, entry] of Object.entries(threadIndex)) {
1406
1827
  if (entry.sessionId === sessionId) {
@@ -1454,6 +1875,19 @@ export class SessionManager {
1454
1875
  throw e;
1455
1876
  }
1456
1877
  }
1878
+ if (fs.existsSync(threadsDir)) {
1879
+ for (const file of fs.readdirSync(threadsDir).filter(name => /\.fork-history\.json(?:\.bak|\.tmp)?$/.test(name))) {
1880
+ const src = path.join(threadsDir, file);
1881
+ const dst = path.join(trashDir, file);
1882
+ try {
1883
+ fs.renameSync(src, dst);
1884
+ }
1885
+ catch (e) {
1886
+ if (e.code !== 'ENOENT')
1887
+ throw e;
1888
+ }
1889
+ }
1890
+ }
1457
1891
  // Clear active.json
1458
1892
  const activePath = path.join(dirPath, 'active.json');
1459
1893
  try {
@@ -1501,6 +1935,10 @@ export class SessionManager {
1501
1935
  if (identityMetadata?.sessionBoundaryOperationId) {
1502
1936
  metadata.sessionBoundaryOperationId = identityMetadata.sessionBoundaryOperationId;
1503
1937
  }
1938
+ // Only an explicit caller may bind a diagnostic case. Never inherit a
1939
+ // previous case (or any provider thread binding) from the active session.
1940
+ if (identityMetadata?.diagnostic)
1941
+ metadata.diagnostic = { ...identityMetadata.diagnostic };
1504
1942
  const session = {
1505
1943
  id: generateSessionId(),
1506
1944
  channel,
@@ -1589,6 +2027,8 @@ export class SessionManager {
1589
2027
  const threadIndex = readThreadIndex(chatDir);
1590
2028
  if (threadIndex[targetThreadId])
1591
2029
  throw new Error('target thread already exists');
2030
+ const historyContent = this.forkHistoryText(options.historySnapshot, options.sourceAssistantMessageId);
2031
+ const historyDigest = this.forkHistoryDigest(historyContent);
1592
2032
  const now = Date.now();
1593
2033
  const sessionKey = formatSessionKey(channelType, sourceSession.channelId, targetThreadId);
1594
2034
  const sourceMetadata = sourceSession.metadata || {};
@@ -1602,9 +2042,14 @@ export class SessionManager {
1602
2042
  sessionId: sourceSession.id,
1603
2043
  threadId: sourceSession.threadId || null,
1604
2044
  assistantMessageId: options.sourceAssistantMessageId,
2045
+ ...(options.requestedSourceAssistantMessageId
2046
+ ? { requestedAssistantMessageId: options.requestedSourceAssistantMessageId } : {}),
1605
2047
  turn: options.sourceTurn,
1606
2048
  name: sourceSession.name || null,
1607
2049
  createdAt: now,
2050
+ historyVersion: 1,
2051
+ historyCount: options.historySnapshot.items.length,
2052
+ historyDigest,
1608
2053
  ...(options.requestId ? { requestId: options.requestId } : {}),
1609
2054
  },
1610
2055
  };
@@ -1622,12 +2067,17 @@ export class SessionManager {
1622
2067
  agentSessionId: forkedAgentSessionId,
1623
2068
  metadata,
1624
2069
  name: options.name || `${sourceSession.name || '会话'} · 第 ${options.sourceTurn} 轮`,
1625
- lable: normalizeTopicLable(options.lable === undefined ? sourceSession.lable : options.lable),
2070
+ label: normalizeTopicLabel(options.label === undefined ? sourceSession.label : options.label),
2071
+ topicStatus: options.topicStatus ? normalizeTopicStatus(options.topicStatus) : 'pending',
1626
2072
  createdAt: now,
1627
2073
  updatedAt: now,
1628
2074
  };
1629
- this.persistSession(session, 'none');
2075
+ const historyFile = this.forkHistoryPath(session);
2076
+ const historyBackup = this.forkHistoryBackupPath(session);
1630
2077
  try {
2078
+ atomicWriteText(historyFile, historyContent);
2079
+ atomicWriteText(historyBackup, historyContent);
2080
+ this.persistSession(session, 'none');
1631
2081
  threadIndex[targetThreadId] = {
1632
2082
  sessionId: session.id,
1633
2083
  sessionKey,
@@ -1640,6 +2090,14 @@ export class SessionManager {
1640
2090
  fs.unlinkSync(this.metaPathForSession(session));
1641
2091
  }
1642
2092
  catch { }
2093
+ try {
2094
+ fs.unlinkSync(historyFile);
2095
+ }
2096
+ catch { }
2097
+ try {
2098
+ fs.unlinkSync(historyBackup);
2099
+ }
2100
+ catch { }
1643
2101
  throw error;
1644
2102
  }
1645
2103
  this.eventBus.publish({
@@ -1696,6 +2154,13 @@ export class SessionManager {
1696
2154
  cause: 'renew',
1697
2155
  sourceSessionId: sourceSession.id,
1698
2156
  renewSource: context?.source,
2157
+ // 渠道快照:供记忆整理触发用(不依赖旧会话是否还有活动 taskCtx)
2158
+ sourceChannel: sourceSession.channelType || sourceSession.channel,
2159
+ sourceChannelId: sourceSession.channelId,
2160
+ sourceChatType: sourceSession.chatType,
2161
+ sourceThreadId: sourceSession.threadId,
2162
+ sourceSelfAid: sourceSession.selfAID,
2163
+ sourcePeerId: sourceSession.metadata?.peerId,
1699
2164
  timestamp: Date.now(),
1700
2165
  causation: context?.causation,
1701
2166
  });
@@ -1766,7 +2231,7 @@ export class SessionManager {
1766
2231
  return adapter.getFileInfo(projectPath, agentSessionId, agentAid);
1767
2232
  }
1768
2233
  getRunnerTaskHistory(sessionId) {
1769
- return readRunnerTaskHistory(resolvePaths().logs, sessionId);
2234
+ return readRunnerEventSummary(resolvePaths().logs, sessionId);
1770
2235
  }
1771
2236
  async listSdkSessions(projectPath, baseagent, agentAid) {
1772
2237
  const adapter = this.getFileAdapter(baseagent);