evolcore 0.0.28 → 0.0.29
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.
- package/CHANGELOG.md +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- 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 {
|
|
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,
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
session.
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1380
|
-
const normalized =
|
|
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.
|
|
1385
|
-
this.persistSession(loaded.current, 'sync', {
|
|
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 {
|
|
@@ -1593,6 +2027,8 @@ export class SessionManager {
|
|
|
1593
2027
|
const threadIndex = readThreadIndex(chatDir);
|
|
1594
2028
|
if (threadIndex[targetThreadId])
|
|
1595
2029
|
throw new Error('target thread already exists');
|
|
2030
|
+
const historyContent = this.forkHistoryText(options.historySnapshot, options.sourceAssistantMessageId);
|
|
2031
|
+
const historyDigest = this.forkHistoryDigest(historyContent);
|
|
1596
2032
|
const now = Date.now();
|
|
1597
2033
|
const sessionKey = formatSessionKey(channelType, sourceSession.channelId, targetThreadId);
|
|
1598
2034
|
const sourceMetadata = sourceSession.metadata || {};
|
|
@@ -1606,9 +2042,14 @@ export class SessionManager {
|
|
|
1606
2042
|
sessionId: sourceSession.id,
|
|
1607
2043
|
threadId: sourceSession.threadId || null,
|
|
1608
2044
|
assistantMessageId: options.sourceAssistantMessageId,
|
|
2045
|
+
...(options.requestedSourceAssistantMessageId
|
|
2046
|
+
? { requestedAssistantMessageId: options.requestedSourceAssistantMessageId } : {}),
|
|
1609
2047
|
turn: options.sourceTurn,
|
|
1610
2048
|
name: sourceSession.name || null,
|
|
1611
2049
|
createdAt: now,
|
|
2050
|
+
historyVersion: 1,
|
|
2051
|
+
historyCount: options.historySnapshot.items.length,
|
|
2052
|
+
historyDigest,
|
|
1612
2053
|
...(options.requestId ? { requestId: options.requestId } : {}),
|
|
1613
2054
|
},
|
|
1614
2055
|
};
|
|
@@ -1626,12 +2067,17 @@ export class SessionManager {
|
|
|
1626
2067
|
agentSessionId: forkedAgentSessionId,
|
|
1627
2068
|
metadata,
|
|
1628
2069
|
name: options.name || `${sourceSession.name || '会话'} · 第 ${options.sourceTurn} 轮`,
|
|
1629
|
-
|
|
2070
|
+
label: normalizeTopicLabel(options.label === undefined ? sourceSession.label : options.label),
|
|
2071
|
+
topicStatus: options.topicStatus ? normalizeTopicStatus(options.topicStatus) : 'pending',
|
|
1630
2072
|
createdAt: now,
|
|
1631
2073
|
updatedAt: now,
|
|
1632
2074
|
};
|
|
1633
|
-
this.
|
|
2075
|
+
const historyFile = this.forkHistoryPath(session);
|
|
2076
|
+
const historyBackup = this.forkHistoryBackupPath(session);
|
|
1634
2077
|
try {
|
|
2078
|
+
atomicWriteText(historyFile, historyContent);
|
|
2079
|
+
atomicWriteText(historyBackup, historyContent);
|
|
2080
|
+
this.persistSession(session, 'none');
|
|
1635
2081
|
threadIndex[targetThreadId] = {
|
|
1636
2082
|
sessionId: session.id,
|
|
1637
2083
|
sessionKey,
|
|
@@ -1644,6 +2090,14 @@ export class SessionManager {
|
|
|
1644
2090
|
fs.unlinkSync(this.metaPathForSession(session));
|
|
1645
2091
|
}
|
|
1646
2092
|
catch { }
|
|
2093
|
+
try {
|
|
2094
|
+
fs.unlinkSync(historyFile);
|
|
2095
|
+
}
|
|
2096
|
+
catch { }
|
|
2097
|
+
try {
|
|
2098
|
+
fs.unlinkSync(historyBackup);
|
|
2099
|
+
}
|
|
2100
|
+
catch { }
|
|
1647
2101
|
throw error;
|
|
1648
2102
|
}
|
|
1649
2103
|
this.eventBus.publish({
|
|
@@ -1700,6 +2154,13 @@ export class SessionManager {
|
|
|
1700
2154
|
cause: 'renew',
|
|
1701
2155
|
sourceSessionId: sourceSession.id,
|
|
1702
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,
|
|
1703
2164
|
timestamp: Date.now(),
|
|
1704
2165
|
causation: context?.causation,
|
|
1705
2166
|
});
|
|
@@ -1770,7 +2231,7 @@ export class SessionManager {
|
|
|
1770
2231
|
return adapter.getFileInfo(projectPath, agentSessionId, agentAid);
|
|
1771
2232
|
}
|
|
1772
2233
|
getRunnerTaskHistory(sessionId) {
|
|
1773
|
-
return
|
|
2234
|
+
return readRunnerEventSummary(resolvePaths().logs, sessionId);
|
|
1774
2235
|
}
|
|
1775
2236
|
async listSdkSessions(projectPath, baseagent, agentAid) {
|
|
1776
2237
|
const adapter = this.getFileAdapter(baseagent);
|