@adhdev/daemon-core 0.8.27 → 0.8.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/dist/agent-stream/manager.d.ts +1 -1
- package/dist/agent-stream/provider-adapter.d.ts +5 -0
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/router.d.ts +5 -0
- package/dist/commands/stream-commands.d.ts +1 -1
- package/dist/config/chat-history.d.ts +12 -0
- package/dist/detection/cli-detector.d.ts +6 -2
- package/dist/detection/ide-detector.d.ts +2 -1
- package/dist/index.js +987 -377
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +985 -375
- package/dist/index.mjs.map +1 -1
- package/dist/providers/acp-provider-instance.d.ts +1 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/providers/provider-loader.d.ts +26 -0
- package/dist/shared-types.d.ts +2 -0
- package/dist/status/snapshot.d.ts +8 -0
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +24 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +24 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js +6 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs +6 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/agent-stream/manager.ts +2 -2
- package/src/agent-stream/provider-adapter.ts +111 -4
- package/src/boot/daemon-lifecycle.ts +28 -1
- package/src/cli-adapters/provider-cli-adapter.ts +17 -3
- package/src/commands/chat-commands.ts +89 -10
- package/src/commands/cli-manager.ts +16 -2
- package/src/commands/handler.ts +1 -0
- package/src/commands/router.ts +23 -1
- package/src/commands/stream-commands.ts +6 -3
- package/src/config/chat-history.ts +269 -18
- package/src/detection/cli-detector.ts +72 -29
- package/src/detection/ide-detector.ts +24 -8
- package/src/launch.ts +1 -1
- package/src/providers/acp-provider-instance.ts +19 -10
- package/src/providers/cli-provider-instance.ts +17 -2
- package/src/providers/provider-loader.ts +144 -11
- package/src/shared-types.ts +2 -0
- package/src/status/snapshot.ts +19 -1
|
@@ -522,10 +522,10 @@ export class DaemonCliManager {
|
|
|
522
522
|
if (!cliInfo) {
|
|
523
523
|
const installHint = provider?.install || '';
|
|
524
524
|
const displayName = provider?.displayName || provider?.name || cliType;
|
|
525
|
-
const spawnCmd = provider?.spawn?.command || cliType;
|
|
525
|
+
const spawnCmd = this.providerLoader.getSpawnCommand(normalizedType, provider?.spawn?.command || cliType);
|
|
526
526
|
throw new Error(
|
|
527
527
|
`${displayName} is not installed.\n` +
|
|
528
|
-
`Command '${spawnCmd}' not
|
|
528
|
+
`Command '${spawnCmd}' is not available.\n` +
|
|
529
529
|
(installHint ? `\n${installHint}\n` : '') +
|
|
530
530
|
`\nRun 'adhdev doctor' for detailed diagnostics.`
|
|
531
531
|
);
|
|
@@ -687,6 +687,7 @@ export class DaemonCliManager {
|
|
|
687
687
|
if (!instanceManager) return 0;
|
|
688
688
|
const sessions = records || await this.deps.listHostedCliRuntimes?.() || [];
|
|
689
689
|
let restored = 0;
|
|
690
|
+
const restoredBindings = new Set<string>();
|
|
690
691
|
|
|
691
692
|
for (const record of sessions) {
|
|
692
693
|
if (!record?.runtimeId || !record?.cliType || !record?.workspace) continue;
|
|
@@ -702,6 +703,18 @@ export class DaemonCliManager {
|
|
|
702
703
|
record.cliArgs,
|
|
703
704
|
record.providerSessionId,
|
|
704
705
|
);
|
|
706
|
+
const bindingKey = [
|
|
707
|
+
normalizedType,
|
|
708
|
+
record.workspace,
|
|
709
|
+
sessionBinding.providerSessionId || record.runtimeId,
|
|
710
|
+
].join('::');
|
|
711
|
+
if (restoredBindings.has(bindingKey)) {
|
|
712
|
+
LOG.info(
|
|
713
|
+
'CLI',
|
|
714
|
+
`↷ Skipping duplicate hosted runtime restore: ${record.runtimeKey || record.runtimeId} (${normalizedType} @ ${record.workspace}) binding=${sessionBinding.providerSessionId || 'runtime'}`
|
|
715
|
+
);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
705
718
|
try {
|
|
706
719
|
await this.registerCliInstance(
|
|
707
720
|
record.runtimeId,
|
|
@@ -717,6 +730,7 @@ export class DaemonCliManager {
|
|
|
717
730
|
launchMode: 'manual',
|
|
718
731
|
},
|
|
719
732
|
);
|
|
733
|
+
restoredBindings.add(bindingKey);
|
|
720
734
|
restored += 1;
|
|
721
735
|
LOG.info('CLI', `♻ Restored hosted runtime: ${record.runtimeKey || record.runtimeId} (${record.displayName || record.workspace})`);
|
|
722
736
|
} catch (error: any) {
|
package/src/commands/handler.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface CommandContext {
|
|
|
44
44
|
/** ProviderInstanceManager — for runtime settings propagation */
|
|
45
45
|
instanceManager?: ProviderInstanceManager;
|
|
46
46
|
sessionRegistry?: SessionRegistry;
|
|
47
|
+
onProviderSettingChanged?: (providerType: string, key: string, value: any) => Promise<void> | void;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
package/src/commands/router.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface SessionHostControlPlane {
|
|
|
44
44
|
restartSession(sessionId: string): Promise<any>;
|
|
45
45
|
sendSignal(sessionId: string, signal: string): Promise<any>;
|
|
46
46
|
forceDetachClient(sessionId: string, clientId: string): Promise<any>;
|
|
47
|
+
pruneDuplicateSessions(payload?: { providerType?: string; workspace?: string; dryRun?: boolean }): Promise<any>;
|
|
47
48
|
acquireWrite(payload: { sessionId: string; clientId: string; ownerType: 'agent' | 'user'; force?: boolean }): Promise<any>;
|
|
48
49
|
releaseWrite(payload: { sessionId: string; clientId: string }): Promise<any>;
|
|
49
50
|
}
|
|
@@ -265,6 +266,16 @@ export class DaemonCommandRouter {
|
|
|
265
266
|
return { success: true, record };
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
case 'session_host_prune_duplicate_sessions': {
|
|
270
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
271
|
+
const result = await this.deps.sessionHostControl.pruneDuplicateSessions({
|
|
272
|
+
providerType: typeof args?.providerType === 'string' ? args.providerType : undefined,
|
|
273
|
+
workspace: typeof args?.workspace === 'string' ? args.workspace : undefined,
|
|
274
|
+
dryRun: args?.dryRun === true,
|
|
275
|
+
});
|
|
276
|
+
return { success: true, result };
|
|
277
|
+
}
|
|
278
|
+
|
|
268
279
|
case 'session_host_acquire_write': {
|
|
269
280
|
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
270
281
|
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
@@ -365,6 +376,11 @@ export class DaemonCommandRouter {
|
|
|
365
376
|
if (!ideType) throw new Error('ideType required');
|
|
366
377
|
const killProcess = args?.killProcess !== false; // default true
|
|
367
378
|
await this.stopIde(ideType, killProcess);
|
|
379
|
+
try {
|
|
380
|
+
const results = await detectIDEs(this.deps.providerLoader);
|
|
381
|
+
this.deps.detectedIdes.value = results;
|
|
382
|
+
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
383
|
+
} catch { /* ignore detection refresh errors */ }
|
|
368
384
|
return { success: true, ideType, stopped: true, processKilled: killProcess };
|
|
369
385
|
}
|
|
370
386
|
|
|
@@ -415,6 +431,11 @@ export class DaemonCommandRouter {
|
|
|
415
431
|
}
|
|
416
432
|
}
|
|
417
433
|
this.deps.onIdeConnected?.();
|
|
434
|
+
try {
|
|
435
|
+
const results = await detectIDEs(this.deps.providerLoader);
|
|
436
|
+
this.deps.detectedIdes.value = results;
|
|
437
|
+
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
438
|
+
} catch { /* ignore detection refresh errors */ }
|
|
418
439
|
if (result.success && resolvedWorkspace) {
|
|
419
440
|
try {
|
|
420
441
|
const next = appendRecentActivity(loadState(), {
|
|
@@ -441,8 +462,9 @@ export class DaemonCommandRouter {
|
|
|
441
462
|
|
|
442
463
|
// ─── Detect IDEs ───
|
|
443
464
|
case 'detect_ides': {
|
|
444
|
-
const results = await detectIDEs();
|
|
465
|
+
const results = await detectIDEs(this.deps.providerLoader);
|
|
445
466
|
this.deps.detectedIdes.value = results;
|
|
467
|
+
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
446
468
|
return { success: true, detectedInfo: results };
|
|
447
469
|
}
|
|
448
470
|
|
|
@@ -76,7 +76,7 @@ export function handleGetProviderSettings(h: CommandHelpers, args: any): Command
|
|
|
76
76
|
return { success: true, settings: allSettings, values: allValues };
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export function handleSetProviderSetting(h: CommandHelpers, args: any): CommandResult {
|
|
79
|
+
export async function handleSetProviderSetting(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
80
80
|
const loader = h.ctx.providerLoader as ProviderLoader | undefined;
|
|
81
81
|
const { providerType, key, value } = args || {};
|
|
82
82
|
if (!providerType || !key || value === undefined) {
|
|
@@ -89,6 +89,7 @@ export function handleSetProviderSetting(h: CommandHelpers, args: any): CommandR
|
|
|
89
89
|
const updated = h.ctx.instanceManager.updateInstanceSettings(providerType, allSettings);
|
|
90
90
|
LOG.info('Command', `[set_provider_setting] ${providerType}.${key}=${JSON.stringify(value)} → ${updated} instance(s) updated`);
|
|
91
91
|
}
|
|
92
|
+
await h.ctx.onProviderSettingChanged?.(providerType, key, value);
|
|
92
93
|
return { success: true, providerType, key, value };
|
|
93
94
|
}
|
|
94
95
|
return { success: false, error: `Failed to set ${providerType}.${key} — invalid key, value, or not a public setting` };
|
|
@@ -133,7 +134,7 @@ function getCliScriptCommand(payload: any): { type: string; text?: string } | nu
|
|
|
133
134
|
|
|
134
135
|
const command = payload.command;
|
|
135
136
|
if (!command || typeof command !== 'object') return null;
|
|
136
|
-
if (command.type !== 'send_message') return null;
|
|
137
|
+
if (command.type !== 'send_message' && command.type !== 'pty_write') return null;
|
|
137
138
|
|
|
138
139
|
const text = typeof command.text === 'string'
|
|
139
140
|
? command.text.trim()
|
|
@@ -141,7 +142,7 @@ function getCliScriptCommand(payload: any): { type: string; text?: string } | nu
|
|
|
141
142
|
? command.message.trim()
|
|
142
143
|
: '';
|
|
143
144
|
if (!text) return null;
|
|
144
|
-
return { type:
|
|
145
|
+
return { type: command.type, text };
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
function applyProviderPatch(h: CommandHelpers, args: any, payload: any): void {
|
|
@@ -191,6 +192,8 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
|
|
|
191
192
|
const cliCommand = getCliScriptCommand(parsed.payload);
|
|
192
193
|
if (cliCommand?.type === 'send_message' && cliCommand.text) {
|
|
193
194
|
await adapter.sendMessage(cliCommand.text);
|
|
195
|
+
} else if (cliCommand?.type === 'pty_write' && cliCommand.text && adapter.writeRaw) {
|
|
196
|
+
adapter.writeRaw(cliCommand.text + '\r');
|
|
194
197
|
}
|
|
195
198
|
applyProviderPatch(h, args, parsed.payload);
|
|
196
199
|
return { success: true, ...(parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : { result: parsed.payload }) };
|
|
@@ -29,6 +29,93 @@ interface HistoryMessage {
|
|
|
29
29
|
sessionTitle?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const CODEX_STARTER_PROMPT_RE = /^(?:[›❯]\s*)?(?:Find and fix a bug in @filename|Improve documentation in @filename|Write tests for @filename|Explain this codebase|Summarize recent commits|Implement \{feature\}|Use \/skills(?: to list available skills)?|Run \/review on my current changes)$/i;
|
|
33
|
+
|
|
34
|
+
function normalizeHistoryComparable(text: string): string {
|
|
35
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanupHistoryContent(agentType: string, role: HistoryMessage['role'], content: string): string {
|
|
39
|
+
let value = String(content || '').replace(/\r\n/g, '\n').trim();
|
|
40
|
+
if (!value) return '';
|
|
41
|
+
|
|
42
|
+
if (agentType === 'codex-cli' && role === 'assistant') {
|
|
43
|
+
const filtered = value
|
|
44
|
+
.split('\n')
|
|
45
|
+
.filter((line) => !CODEX_STARTER_PROMPT_RE.test(line.trim()))
|
|
46
|
+
.join('\n')
|
|
47
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
48
|
+
.trim();
|
|
49
|
+
value = filtered;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildHistoryMessageHash(
|
|
56
|
+
agentType: string,
|
|
57
|
+
message: Pick<HistoryMessage, 'role' | 'content' | 'receivedAt' | 'kind'> & { historyDedupKey?: string },
|
|
58
|
+
): string {
|
|
59
|
+
if (message.historyDedupKey) return message.historyDedupKey;
|
|
60
|
+
const cleaned = cleanupHistoryContent(agentType, message.role, message.content);
|
|
61
|
+
return `${message.kind || 'standard'}:${message.role}:${message.receivedAt || 0}:${normalizeHistoryComparable(cleaned)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildHistoryMessageSignature(
|
|
65
|
+
agentType: string,
|
|
66
|
+
message: Pick<HistoryMessage, 'role' | 'content' | 'kind'>,
|
|
67
|
+
): string {
|
|
68
|
+
const cleaned = cleanupHistoryContent(agentType, message.role, message.content);
|
|
69
|
+
return `${message.kind || 'standard'}:${message.role}:${normalizeHistoryComparable(cleaned)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isAdjacentHistoryDuplicate(
|
|
73
|
+
agentType: string,
|
|
74
|
+
previous: Pick<HistoryMessage, 'role' | 'content' | 'kind'> | null | undefined,
|
|
75
|
+
next: Pick<HistoryMessage, 'role' | 'content' | 'kind'> | null | undefined,
|
|
76
|
+
): boolean {
|
|
77
|
+
if (!previous || !next) return false;
|
|
78
|
+
return buildHistoryMessageSignature(agentType, previous) === buildHistoryMessageSignature(agentType, next);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collapseReplayAssistantTurns(agentType: string, messages: HistoryMessage[]): HistoryMessage[] {
|
|
82
|
+
if (agentType !== 'codex-cli') return messages;
|
|
83
|
+
|
|
84
|
+
const collapsed: HistoryMessage[] = [];
|
|
85
|
+
let sawAssistantSinceLastUser = false;
|
|
86
|
+
|
|
87
|
+
for (const message of messages) {
|
|
88
|
+
if (message.role === 'user') {
|
|
89
|
+
sawAssistantSinceLastUser = false;
|
|
90
|
+
collapsed.push(message);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (message.role === 'assistant') {
|
|
95
|
+
if (sawAssistantSinceLastUser) continue;
|
|
96
|
+
sawAssistantSinceLastUser = true;
|
|
97
|
+
collapsed.push(message);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
collapsed.push(message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return collapsed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sanitizeHistoryMessage(agentType: string, message: HistoryMessage): HistoryMessage | null {
|
|
108
|
+
if (!message || (message.role !== 'user' && message.role !== 'assistant' && message.role !== 'system')) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const content = cleanupHistoryContent(agentType, message.role, message.content);
|
|
112
|
+
if (!content) return null;
|
|
113
|
+
return {
|
|
114
|
+
...message,
|
|
115
|
+
content,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
32
119
|
export interface SavedHistorySessionSummary {
|
|
33
120
|
historySessionId: string;
|
|
34
121
|
sessionTitle?: string;
|
|
@@ -43,6 +130,10 @@ export class ChatHistoryWriter {
|
|
|
43
130
|
private lastSeenCounts = new Map<string, number>();
|
|
44
131
|
/** Last seen message hash per agent (deduplication) */
|
|
45
132
|
private lastSeenHashes = new Map<string, Set<string>>();
|
|
133
|
+
/** Last appended normalized message signature per agent/session */
|
|
134
|
+
private lastSeenSignatures = new Map<string, string>();
|
|
135
|
+
/** Last appended normalized non-system turn signature per agent/session */
|
|
136
|
+
private lastSeenTurnSignatures = new Map<string, string>();
|
|
46
137
|
private rotated = false;
|
|
47
138
|
|
|
48
139
|
/**
|
|
@@ -75,14 +166,36 @@ export class ChatHistoryWriter {
|
|
|
75
166
|
// Filter new messages
|
|
76
167
|
const newMessages: HistoryMessage[] = [];
|
|
77
168
|
for (const msg of messages) {
|
|
78
|
-
const
|
|
169
|
+
const role = msg.role as 'user' | 'assistant' | 'system';
|
|
170
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;
|
|
171
|
+
const content = cleanupHistoryContent(agentType, role, msg.content || '');
|
|
172
|
+
if (!content) continue;
|
|
173
|
+
const receivedAt = msg.receivedAt || Date.now();
|
|
174
|
+
const hash = buildHistoryMessageHash(agentType, {
|
|
175
|
+
role,
|
|
176
|
+
content,
|
|
177
|
+
receivedAt,
|
|
178
|
+
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
179
|
+
historyDedupKey: msg.historyDedupKey,
|
|
180
|
+
});
|
|
181
|
+
const signature = buildHistoryMessageSignature(agentType, {
|
|
182
|
+
role,
|
|
183
|
+
content,
|
|
184
|
+
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
185
|
+
});
|
|
79
186
|
if (seenHashes.has(hash)) continue;
|
|
187
|
+
if (this.lastSeenSignatures.get(dedupKey) === signature) continue;
|
|
188
|
+
if (role !== 'system' && this.lastSeenTurnSignatures.get(dedupKey) === signature) continue;
|
|
80
189
|
seenHashes.add(hash);
|
|
190
|
+
this.lastSeenSignatures.set(dedupKey, signature);
|
|
191
|
+
if (role !== 'system') {
|
|
192
|
+
this.lastSeenTurnSignatures.set(dedupKey, signature);
|
|
193
|
+
}
|
|
81
194
|
newMessages.push({
|
|
82
|
-
ts: new Date(
|
|
83
|
-
receivedAt
|
|
84
|
-
role
|
|
85
|
-
content
|
|
195
|
+
ts: new Date(receivedAt).toISOString(),
|
|
196
|
+
receivedAt,
|
|
197
|
+
role,
|
|
198
|
+
content,
|
|
86
199
|
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
87
200
|
senderName: typeof msg.senderName === 'string' ? msg.senderName : undefined,
|
|
88
201
|
agent: agentType,
|
|
@@ -108,6 +221,8 @@ export class ChatHistoryWriter {
|
|
|
108
221
|
const prevCount = this.lastSeenCounts.get(dedupKey) || 0;
|
|
109
222
|
if (messages.length < prevCount * 0.5 && prevCount > 3) {
|
|
110
223
|
seenHashes.clear();
|
|
224
|
+
this.lastSeenSignatures.delete(dedupKey);
|
|
225
|
+
this.lastSeenTurnSignatures.delete(dedupKey);
|
|
111
226
|
for (const msg of messages) {
|
|
112
227
|
seenHashes.add(msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`);
|
|
113
228
|
}
|
|
@@ -124,6 +239,62 @@ export class ChatHistoryWriter {
|
|
|
124
239
|
}
|
|
125
240
|
}
|
|
126
241
|
|
|
242
|
+
seedSessionHistory(
|
|
243
|
+
agentType: string,
|
|
244
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; historyDedupKey?: string }> = [],
|
|
245
|
+
historySessionId?: string,
|
|
246
|
+
instanceId?: string,
|
|
247
|
+
): void {
|
|
248
|
+
const effectiveHistoryKey = historySessionId || instanceId;
|
|
249
|
+
const dedupKey = effectiveHistoryKey ? `${agentType}:${effectiveHistoryKey}` : agentType;
|
|
250
|
+
const seenHashes = new Set<string>();
|
|
251
|
+
|
|
252
|
+
for (const raw of messages) {
|
|
253
|
+
const role = raw?.role as 'user' | 'assistant' | 'system';
|
|
254
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;
|
|
255
|
+
const content = cleanupHistoryContent(agentType, role, raw?.content || '');
|
|
256
|
+
if (!content) continue;
|
|
257
|
+
seenHashes.add(buildHistoryMessageHash(agentType, {
|
|
258
|
+
role,
|
|
259
|
+
content,
|
|
260
|
+
receivedAt: raw?.receivedAt || 0,
|
|
261
|
+
kind: typeof raw?.kind === 'string' ? raw.kind : undefined,
|
|
262
|
+
historyDedupKey: raw?.historyDedupKey,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.lastSeenHashes.set(dedupKey, seenHashes);
|
|
267
|
+
this.lastSeenCounts.set(dedupKey, messages.length);
|
|
268
|
+
const lastMessage = [...messages].reverse().find((raw) => {
|
|
269
|
+
const role = raw?.role as 'user' | 'assistant' | 'system';
|
|
270
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') return false;
|
|
271
|
+
return !!cleanupHistoryContent(agentType, role, raw?.content || '');
|
|
272
|
+
});
|
|
273
|
+
const lastTurnMessage = [...messages].reverse().find((raw) => {
|
|
274
|
+
const role = raw?.role as 'user' | 'assistant';
|
|
275
|
+
if (role !== 'user' && role !== 'assistant') return false;
|
|
276
|
+
return !!cleanupHistoryContent(agentType, role, raw?.content || '');
|
|
277
|
+
});
|
|
278
|
+
if (lastMessage) {
|
|
279
|
+
this.lastSeenSignatures.set(dedupKey, buildHistoryMessageSignature(agentType, {
|
|
280
|
+
role: lastMessage.role as HistoryMessage['role'],
|
|
281
|
+
content: lastMessage.content,
|
|
282
|
+
kind: typeof lastMessage.kind === 'string' ? lastMessage.kind : undefined,
|
|
283
|
+
}));
|
|
284
|
+
} else {
|
|
285
|
+
this.lastSeenSignatures.delete(dedupKey);
|
|
286
|
+
}
|
|
287
|
+
if (lastTurnMessage) {
|
|
288
|
+
this.lastSeenTurnSignatures.set(dedupKey, buildHistoryMessageSignature(agentType, {
|
|
289
|
+
role: lastTurnMessage.role as 'user' | 'assistant',
|
|
290
|
+
content: lastTurnMessage.content,
|
|
291
|
+
kind: typeof lastTurnMessage.kind === 'string' ? lastTurnMessage.kind : undefined,
|
|
292
|
+
}));
|
|
293
|
+
} else {
|
|
294
|
+
this.lastSeenTurnSignatures.delete(dedupKey);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
127
298
|
appendSystemMarker(
|
|
128
299
|
agentType: string,
|
|
129
300
|
content: string,
|
|
@@ -171,6 +342,16 @@ export class ChatHistoryWriter {
|
|
|
171
342
|
this.lastSeenHashes.set(toDedupKey, nextHashes);
|
|
172
343
|
this.lastSeenHashes.delete(fromDedupKey);
|
|
173
344
|
}
|
|
345
|
+
const fromSignature = this.lastSeenSignatures.get(fromDedupKey);
|
|
346
|
+
if (fromSignature) {
|
|
347
|
+
this.lastSeenSignatures.set(toDedupKey, fromSignature);
|
|
348
|
+
this.lastSeenSignatures.delete(fromDedupKey);
|
|
349
|
+
}
|
|
350
|
+
const fromTurnSignature = this.lastSeenTurnSignatures.get(fromDedupKey);
|
|
351
|
+
if (fromTurnSignature) {
|
|
352
|
+
this.lastSeenTurnSignatures.set(toDedupKey, fromTurnSignature);
|
|
353
|
+
this.lastSeenTurnSignatures.delete(fromDedupKey);
|
|
354
|
+
}
|
|
174
355
|
const fromCount = this.lastSeenCounts.get(fromDedupKey);
|
|
175
356
|
if (typeof fromCount === 'number') {
|
|
176
357
|
this.lastSeenCounts.set(toDedupKey, Math.max(fromCount, this.lastSeenCounts.get(toDedupKey) || 0));
|
|
@@ -221,10 +402,69 @@ export class ChatHistoryWriter {
|
|
|
221
402
|
}
|
|
222
403
|
}
|
|
223
404
|
|
|
405
|
+
compactHistorySession(agentType: string, historySessionId: string): void {
|
|
406
|
+
const sessionId = String(historySessionId || '').trim();
|
|
407
|
+
if (!sessionId) return;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
|
|
411
|
+
if (!fs.existsSync(dir)) return;
|
|
412
|
+
|
|
413
|
+
const prefix = `${this.sanitize(sessionId)}_`;
|
|
414
|
+
const files = fs.readdirSync(dir)
|
|
415
|
+
.filter((file) => file.startsWith(prefix) && file.endsWith('.jsonl'))
|
|
416
|
+
.sort();
|
|
417
|
+
|
|
418
|
+
const seen = new Set<string>();
|
|
419
|
+
for (const file of files) {
|
|
420
|
+
const filePath = path.join(dir, file);
|
|
421
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
|
422
|
+
const next: HistoryMessage[] = [];
|
|
423
|
+
|
|
424
|
+
for (const line of lines) {
|
|
425
|
+
let parsed: HistoryMessage | null = null;
|
|
426
|
+
try {
|
|
427
|
+
parsed = JSON.parse(line) as HistoryMessage;
|
|
428
|
+
} catch {
|
|
429
|
+
parsed = null;
|
|
430
|
+
}
|
|
431
|
+
if (!parsed || parsed.historySessionId !== sessionId) continue;
|
|
432
|
+
const sanitized = sanitizeHistoryMessage(agentType, parsed);
|
|
433
|
+
if (!sanitized) continue;
|
|
434
|
+
const hash = buildHistoryMessageHash(agentType, sanitized);
|
|
435
|
+
if (seen.has(hash)) continue;
|
|
436
|
+
seen.add(hash);
|
|
437
|
+
next.push(sanitized);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
next.sort((a, b) => a.receivedAt - b.receivedAt);
|
|
441
|
+
const dedupedAdjacent: HistoryMessage[] = [];
|
|
442
|
+
let lastTurn: HistoryMessage | null = null;
|
|
443
|
+
for (const entry of next) {
|
|
444
|
+
const previous = dedupedAdjacent[dedupedAdjacent.length - 1];
|
|
445
|
+
if (isAdjacentHistoryDuplicate(agentType, previous, entry)) continue;
|
|
446
|
+
if (entry.role !== 'system' && isAdjacentHistoryDuplicate(agentType, lastTurn, entry)) continue;
|
|
447
|
+
dedupedAdjacent.push(entry);
|
|
448
|
+
if (entry.role !== 'system') lastTurn = entry;
|
|
449
|
+
}
|
|
450
|
+
const collapsed = collapseReplayAssistantTurns(agentType, dedupedAdjacent);
|
|
451
|
+
if (collapsed.length === 0) {
|
|
452
|
+
fs.unlinkSync(filePath);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
fs.writeFileSync(filePath, `${collapsed.map((entry) => JSON.stringify(entry)).join('\n')}\n`, 'utf-8');
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
// Ignore compaction failure.
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
224
462
|
/** Called when agent session is explicitly changed */
|
|
225
463
|
onSessionChange(agentType: string): void {
|
|
226
464
|
this.lastSeenHashes.delete(agentType);
|
|
227
465
|
this.lastSeenCounts.delete(agentType);
|
|
466
|
+
this.lastSeenSignatures.delete(agentType);
|
|
467
|
+
this.lastSeenTurnSignatures.delete(agentType);
|
|
228
468
|
}
|
|
229
469
|
|
|
230
470
|
/** Delete history files older than 30 days */
|
|
@@ -293,31 +533,42 @@ export function readChatHistory(
|
|
|
293
533
|
.sort()
|
|
294
534
|
.reverse();
|
|
295
535
|
|
|
296
|
-
// Read lines from all files (reverse order)
|
|
297
536
|
const allMessages: HistoryMessage[] = [];
|
|
298
|
-
const
|
|
537
|
+
const seen = new Set<string>();
|
|
299
538
|
|
|
300
539
|
for (const file of files) {
|
|
301
|
-
if (allMessages.length >= needed) break;
|
|
302
540
|
const filePath = path.join(dir, file);
|
|
303
541
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
304
542
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
308
|
-
if (allMessages.length >= needed) break;
|
|
543
|
+
|
|
544
|
+
for (let i = 0; i < lines.length; i++) {
|
|
309
545
|
try {
|
|
310
|
-
|
|
546
|
+
const parsed = JSON.parse(lines[i]) as HistoryMessage;
|
|
547
|
+
const sanitizedMessage = sanitizeHistoryMessage(agentType, parsed);
|
|
548
|
+
if (!sanitizedMessage) continue;
|
|
549
|
+
const hash = buildHistoryMessageHash(agentType, sanitizedMessage);
|
|
550
|
+
if (seen.has(hash)) continue;
|
|
551
|
+
seen.add(hash);
|
|
552
|
+
allMessages.push(sanitizedMessage);
|
|
311
553
|
} catch { /* skip invalid lines */ }
|
|
312
554
|
}
|
|
313
555
|
}
|
|
314
556
|
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
557
|
+
allMessages.sort((a, b) => a.receivedAt - b.receivedAt);
|
|
558
|
+
const chronological: HistoryMessage[] = [];
|
|
559
|
+
let lastTurn: HistoryMessage | null = null;
|
|
560
|
+
for (const message of allMessages) {
|
|
561
|
+
const previous = chronological[chronological.length - 1];
|
|
562
|
+
if (isAdjacentHistoryDuplicate(agentType, previous, message)) continue;
|
|
563
|
+
if (message.role !== 'system' && isAdjacentHistoryDuplicate(agentType, lastTurn, message)) continue;
|
|
564
|
+
chronological.push(message);
|
|
565
|
+
if (message.role !== 'system') lastTurn = message;
|
|
566
|
+
}
|
|
567
|
+
const collapsed = collapseReplayAssistantTurns(agentType, chronological);
|
|
318
568
|
|
|
319
|
-
//
|
|
320
|
-
sliced.
|
|
569
|
+
// offset/limit apply
|
|
570
|
+
const sliced = collapsed.slice(offset, offset + limit);
|
|
571
|
+
const hasMore = collapsed.length > offset + limit;
|
|
321
572
|
|
|
322
573
|
return { messages: sliced, hasMore };
|
|
323
574
|
} catch {
|