@adhdev/daemon-core 0.7.45 → 0.8.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.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +34 -0
- package/dist/cli-adapters/pty-transport.d.ts +1 -0
- package/dist/cli-adapters/session-host-transport.d.ts +1 -0
- package/dist/commands/cli-manager.d.ts +11 -2
- package/dist/config/chat-history.d.ts +32 -2
- package/dist/config/config.d.ts +5 -1
- package/dist/config/recent-activity.d.ts +3 -1
- package/dist/config/saved-sessions.d.ts +22 -0
- package/dist/daemon/dev-auto-implement.d.ts +18 -2
- package/dist/daemon/dev-cli-debug.d.ts +82 -0
- package/dist/daemon/dev-server.d.ts +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6122 -4038
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6114 -4032
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +29 -1
- package/dist/providers/contracts.d.ts +11 -0
- package/dist/providers/provider-instance.d.ts +1 -0
- package/dist/shared-types.d.ts +2 -0
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js +9 -0
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs +9 -0
- 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/boot/daemon-lifecycle.ts +19 -15
- package/src/cli-adapters/provider-cli-adapter.ts +424 -7
- package/src/cli-adapters/pty-transport.ts +1 -0
- package/src/cli-adapters/session-host-transport.ts +32 -1
- package/src/commands/chat-commands.ts +36 -8
- package/src/commands/cli-manager.ts +259 -22
- package/src/commands/router.ts +52 -1
- package/src/config/chat-history.ts +197 -10
- package/src/config/config.d.ts +4 -0
- package/src/config/config.ts +8 -2
- package/src/config/recent-activity.ts +13 -2
- package/src/config/saved-sessions.ts +73 -0
- package/src/daemon/dev-auto-implement.ts +394 -43
- package/src/daemon/dev-cli-debug.ts +839 -0
- package/src/daemon/dev-server.ts +51 -5
- package/src/index.ts +2 -0
- package/src/providers/cli-provider-instance.ts +283 -4
- package/src/providers/contracts.ts +11 -0
- package/src/providers/provider-instance.d.ts +1 -0
- package/src/providers/provider-instance.ts +1 -0
- package/src/providers/provider-loader.ts +39 -0
- package/src/session-host/runtime-support.ts +1 -0
- package/src/shared-types.d.ts +2 -0
- package/src/shared-types.ts +2 -0
- package/src/status/builders.ts +1 -0
- package/src/status/snapshot.ts +1 -0
package/src/commands/router.ts
CHANGED
|
@@ -13,12 +13,15 @@ import { DaemonCdpManager } from '../cdp/manager.js';
|
|
|
13
13
|
import { registerExtensionProviders } from '../cdp/setup.js';
|
|
14
14
|
import { DaemonCommandHandler } from './handler.js';
|
|
15
15
|
import { DaemonCliManager } from './cli-manager.js';
|
|
16
|
+
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
16
17
|
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
17
18
|
import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
18
19
|
import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
|
|
19
20
|
import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
|
|
20
21
|
import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
|
|
21
|
-
import { appendRecentActivity, markSessionSeen } from '../config/recent-activity.js';
|
|
22
|
+
import { appendRecentActivity, getRecentActivity, markSessionSeen } from '../config/recent-activity.js';
|
|
23
|
+
import { getSavedProviderSessions } from '../config/saved-sessions.js';
|
|
24
|
+
import { listSavedHistorySessions } from '../config/chat-history.js';
|
|
22
25
|
import { detectIDEs } from '../detection/ide-detector.js';
|
|
23
26
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
24
27
|
import { LOG } from '../logging/logger.js';
|
|
@@ -155,6 +158,54 @@ export class DaemonCommandRouter {
|
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
case 'list_saved_sessions': {
|
|
162
|
+
const providerType = typeof args?.providerType === 'string'
|
|
163
|
+
? args.providerType.trim()
|
|
164
|
+
: typeof args?.agentType === 'string'
|
|
165
|
+
? args.agentType.trim()
|
|
166
|
+
: '';
|
|
167
|
+
const kind = args?.kind === 'acp' ? 'acp' : 'cli';
|
|
168
|
+
if (!providerType) {
|
|
169
|
+
return { success: false, error: 'providerType required' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const offset = Math.max(0, Number(args?.offset) || 0);
|
|
173
|
+
const limit = Math.max(1, Math.min(100, Number(args?.limit) || 30));
|
|
174
|
+
const { sessions: historySessions, hasMore } = listSavedHistorySessions(providerType, { offset, limit });
|
|
175
|
+
const config = loadConfig();
|
|
176
|
+
const savedSessions = getSavedProviderSessions(config, { providerType, kind });
|
|
177
|
+
const recentSessions = getRecentActivity(config, 200)
|
|
178
|
+
.filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
|
|
179
|
+
const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
|
|
180
|
+
const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
|
|
181
|
+
const providerMeta = this.deps.providerLoader.getMeta(providerType);
|
|
182
|
+
const canResumeById = supportsExplicitSessionResume(providerMeta?.resume);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
sessions: historySessions.map(session => {
|
|
187
|
+
const saved = savedSessionById.get(session.historySessionId);
|
|
188
|
+
const recent = recentSessionById.get(session.historySessionId);
|
|
189
|
+
return {
|
|
190
|
+
id: session.historySessionId,
|
|
191
|
+
providerSessionId: session.historySessionId,
|
|
192
|
+
providerType,
|
|
193
|
+
providerName: saved?.providerName || recent?.providerName || providerType,
|
|
194
|
+
kind: saved?.kind || recent?.kind || kind,
|
|
195
|
+
title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
|
|
196
|
+
workspace: saved?.workspace || recent?.workspace,
|
|
197
|
+
currentModel: saved?.currentModel || recent?.currentModel,
|
|
198
|
+
preview: session.preview,
|
|
199
|
+
messageCount: session.messageCount,
|
|
200
|
+
firstMessageAt: session.firstMessageAt,
|
|
201
|
+
lastMessageAt: session.lastMessageAt,
|
|
202
|
+
canResume: !!(saved?.workspace || recent?.workspace) && canResumeById,
|
|
203
|
+
};
|
|
204
|
+
}),
|
|
205
|
+
hasMore,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
158
209
|
// ─── restart_session: IDE / CLI / ACP unified ───
|
|
159
210
|
case 'restart_session': {
|
|
160
211
|
const targetType = args?.cliType || args?.agentType || args?.ideType;
|
|
@@ -21,11 +21,23 @@ interface HistoryMessage {
|
|
|
21
21
|
receivedAt: number; // epoch ms
|
|
22
22
|
role: 'user' | 'assistant' | 'system';
|
|
23
23
|
content: string;
|
|
24
|
+
kind?: string;
|
|
25
|
+
senderName?: string;
|
|
24
26
|
agent: string; // e.g. 'antigravity', 'cursor', 'gemini-cli'
|
|
25
27
|
instanceId?: string; // IDE instance UUID (distinguishes windows of the same agent type)
|
|
28
|
+
historySessionId?: string; // Persistent provider-side conversation/session key
|
|
26
29
|
sessionTitle?: string;
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
export interface SavedHistorySessionSummary {
|
|
33
|
+
historySessionId: string;
|
|
34
|
+
sessionTitle?: string;
|
|
35
|
+
messageCount: number;
|
|
36
|
+
firstMessageAt: number;
|
|
37
|
+
lastMessageAt: number;
|
|
38
|
+
preview?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
export class ChatHistoryWriter {
|
|
30
42
|
/** Last seen message count per agent (deduplication) */
|
|
31
43
|
private lastSeenCounts = new Map<string, number>();
|
|
@@ -43,15 +55,17 @@ export class ChatHistoryWriter {
|
|
|
43
55
|
*/
|
|
44
56
|
appendNewMessages(
|
|
45
57
|
agentType: string,
|
|
46
|
-
messages: Array<{ role: string; content: string; receivedAt?: number }>,
|
|
58
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; senderName?: string; historyDedupKey?: string }>,
|
|
47
59
|
sessionTitle?: string,
|
|
48
60
|
instanceId?: string,
|
|
61
|
+
historySessionId?: string,
|
|
49
62
|
): void {
|
|
50
63
|
if (!messages || messages.length === 0) return;
|
|
51
64
|
|
|
52
65
|
try {
|
|
53
|
-
// dedup key: agentType + instanceId
|
|
54
|
-
const
|
|
66
|
+
// dedup key: agentType + persistent history key (fallback: runtime instanceId)
|
|
67
|
+
const effectiveHistoryKey = historySessionId || instanceId;
|
|
68
|
+
const dedupKey = effectiveHistoryKey ? `${agentType}:${effectiveHistoryKey}` : agentType;
|
|
55
69
|
let seenHashes = this.lastSeenHashes.get(dedupKey);
|
|
56
70
|
if (!seenHashes) {
|
|
57
71
|
seenHashes = new Set<string>();
|
|
@@ -61,7 +75,7 @@ export class ChatHistoryWriter {
|
|
|
61
75
|
// Filter new messages
|
|
62
76
|
const newMessages: HistoryMessage[] = [];
|
|
63
77
|
for (const msg of messages) {
|
|
64
|
-
const hash = `${msg.role}:${(msg.content || '').slice(0, 50)}`;
|
|
78
|
+
const hash = msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`;
|
|
65
79
|
if (seenHashes.has(hash)) continue;
|
|
66
80
|
seenHashes.add(hash);
|
|
67
81
|
newMessages.push({
|
|
@@ -69,20 +83,23 @@ export class ChatHistoryWriter {
|
|
|
69
83
|
receivedAt: msg.receivedAt || Date.now(),
|
|
70
84
|
role: msg.role as 'user' | 'assistant' | 'system',
|
|
71
85
|
content: msg.content || '',
|
|
86
|
+
kind: typeof msg.kind === 'string' ? msg.kind : undefined,
|
|
87
|
+
senderName: typeof msg.senderName === 'string' ? msg.senderName : undefined,
|
|
72
88
|
agent: agentType,
|
|
73
89
|
instanceId,
|
|
90
|
+
historySessionId: effectiveHistoryKey,
|
|
74
91
|
sessionTitle,
|
|
75
92
|
});
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
if (newMessages.length === 0) return;
|
|
79
96
|
|
|
80
|
-
// Append to file —
|
|
97
|
+
// Append to file — keyed by persistent history session when available
|
|
81
98
|
const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
|
|
82
99
|
fs.mkdirSync(dir, { recursive: true });
|
|
83
100
|
|
|
84
101
|
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
85
|
-
const filePrefix =
|
|
102
|
+
const filePrefix = effectiveHistoryKey ? `${this.sanitize(effectiveHistoryKey)}_` : '';
|
|
86
103
|
const filePath = path.join(dir, `${filePrefix}${date}.jsonl`);
|
|
87
104
|
const lines = newMessages.map(m => JSON.stringify(m)).join('\n') + '\n';
|
|
88
105
|
fs.appendFileSync(filePath, lines, 'utf-8');
|
|
@@ -92,7 +109,7 @@ export class ChatHistoryWriter {
|
|
|
92
109
|
if (messages.length < prevCount * 0.5 && prevCount > 3) {
|
|
93
110
|
seenHashes.clear();
|
|
94
111
|
for (const msg of messages) {
|
|
95
|
-
seenHashes.add(`${msg.role}:${(msg.content || '').slice(0, 50)}`);
|
|
112
|
+
seenHashes.add(msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`);
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
115
|
this.lastSeenCounts.set(dedupKey, messages.length);
|
|
@@ -107,6 +124,103 @@ export class ChatHistoryWriter {
|
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
|
|
127
|
+
appendSystemMarker(
|
|
128
|
+
agentType: string,
|
|
129
|
+
content: string,
|
|
130
|
+
options: {
|
|
131
|
+
sessionTitle?: string;
|
|
132
|
+
instanceId?: string;
|
|
133
|
+
historySessionId?: string;
|
|
134
|
+
dedupKey?: string;
|
|
135
|
+
receivedAt?: number;
|
|
136
|
+
senderName?: string;
|
|
137
|
+
} = {},
|
|
138
|
+
): void {
|
|
139
|
+
this.appendNewMessages(
|
|
140
|
+
agentType,
|
|
141
|
+
[{
|
|
142
|
+
role: 'system',
|
|
143
|
+
kind: 'system',
|
|
144
|
+
content,
|
|
145
|
+
receivedAt: options.receivedAt,
|
|
146
|
+
senderName: options.senderName,
|
|
147
|
+
historyDedupKey: options.dedupKey,
|
|
148
|
+
}],
|
|
149
|
+
options.sessionTitle,
|
|
150
|
+
options.instanceId,
|
|
151
|
+
options.historySessionId,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
promoteHistorySession(
|
|
156
|
+
agentType: string,
|
|
157
|
+
previousHistorySessionId: string,
|
|
158
|
+
nextHistorySessionId: string,
|
|
159
|
+
): void {
|
|
160
|
+
const fromId = String(previousHistorySessionId || '').trim();
|
|
161
|
+
const toId = String(nextHistorySessionId || '').trim();
|
|
162
|
+
if (!fromId || !toId || fromId === toId) return;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const fromDedupKey = `${agentType}:${fromId}`;
|
|
166
|
+
const toDedupKey = `${agentType}:${toId}`;
|
|
167
|
+
const fromHashes = this.lastSeenHashes.get(fromDedupKey);
|
|
168
|
+
if (fromHashes?.size) {
|
|
169
|
+
const nextHashes = this.lastSeenHashes.get(toDedupKey) || new Set<string>();
|
|
170
|
+
for (const hash of fromHashes) nextHashes.add(hash);
|
|
171
|
+
this.lastSeenHashes.set(toDedupKey, nextHashes);
|
|
172
|
+
this.lastSeenHashes.delete(fromDedupKey);
|
|
173
|
+
}
|
|
174
|
+
const fromCount = this.lastSeenCounts.get(fromDedupKey);
|
|
175
|
+
if (typeof fromCount === 'number') {
|
|
176
|
+
this.lastSeenCounts.set(toDedupKey, Math.max(fromCount, this.lastSeenCounts.get(toDedupKey) || 0));
|
|
177
|
+
this.lastSeenCounts.delete(fromDedupKey);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
|
|
181
|
+
if (!fs.existsSync(dir)) return;
|
|
182
|
+
|
|
183
|
+
const fromPrefix = `${this.sanitize(fromId)}_`;
|
|
184
|
+
const toPrefix = `${this.sanitize(toId)}_`;
|
|
185
|
+
const files = fs.readdirSync(dir).filter((file) => file.startsWith(fromPrefix) && file.endsWith('.jsonl'));
|
|
186
|
+
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
const sourcePath = path.join(dir, file);
|
|
189
|
+
const targetPath = path.join(dir, `${toPrefix}${file.slice(fromPrefix.length)}`);
|
|
190
|
+
const sourceLines = fs.readFileSync(sourcePath, 'utf-8').split('\n').filter(Boolean);
|
|
191
|
+
const rewritten = sourceLines
|
|
192
|
+
.map((line) => {
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(line) as HistoryMessage;
|
|
195
|
+
if (parsed.historySessionId !== fromId) return null;
|
|
196
|
+
return JSON.stringify({
|
|
197
|
+
...parsed,
|
|
198
|
+
historySessionId: toId,
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.filter((line): line is string => !!line);
|
|
205
|
+
if (rewritten.length === 0) {
|
|
206
|
+
fs.unlinkSync(sourcePath);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const existing = fs.existsSync(targetPath)
|
|
211
|
+
? new Set(fs.readFileSync(targetPath, 'utf-8').split('\n').filter(Boolean))
|
|
212
|
+
: new Set<string>();
|
|
213
|
+
const nextLines = rewritten.filter((line) => !existing.has(line));
|
|
214
|
+
if (nextLines.length > 0) {
|
|
215
|
+
fs.appendFileSync(targetPath, `${nextLines.join('\n')}\n`, 'utf-8');
|
|
216
|
+
}
|
|
217
|
+
fs.unlinkSync(sourcePath);
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// Ignore promotion failure; future messages will still write to the new session key.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
110
224
|
/** Called when agent session is explicitly changed */
|
|
111
225
|
onSessionChange(agentType: string): void {
|
|
112
226
|
this.lastSeenHashes.delete(agentType);
|
|
@@ -157,15 +271,15 @@ export function readChatHistory(
|
|
|
157
271
|
agentType: string,
|
|
158
272
|
offset: number = 0,
|
|
159
273
|
limit: number = 30,
|
|
160
|
-
|
|
274
|
+
historySessionId?: string,
|
|
161
275
|
): { messages: HistoryMessage[]; hasMore: boolean } {
|
|
162
276
|
try {
|
|
163
277
|
const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
164
278
|
const dir = path.join(HISTORY_DIR, sanitized);
|
|
165
279
|
if (!fs.existsSync(dir)) return { messages: [], hasMore: false };
|
|
166
280
|
|
|
167
|
-
// JSONL file list — filter by
|
|
168
|
-
const sanitizedInstance =
|
|
281
|
+
// JSONL file list — filter by persistent history key when specified
|
|
282
|
+
const sanitizedInstance = historySessionId?.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
169
283
|
const files = fs.readdirSync(dir)
|
|
170
284
|
.filter(f => {
|
|
171
285
|
if (!f.endsWith('.jsonl')) return false;
|
|
@@ -210,3 +324,76 @@ export function readChatHistory(
|
|
|
210
324
|
return { messages: [], hasMore: false };
|
|
211
325
|
}
|
|
212
326
|
}
|
|
327
|
+
|
|
328
|
+
export function listSavedHistorySessions(
|
|
329
|
+
agentType: string,
|
|
330
|
+
options: { offset?: number; limit?: number } = {},
|
|
331
|
+
): { sessions: SavedHistorySessionSummary[]; hasMore: boolean } {
|
|
332
|
+
try {
|
|
333
|
+
const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
334
|
+
const dir = path.join(HISTORY_DIR, sanitized);
|
|
335
|
+
if (!fs.existsSync(dir)) return { sessions: [], hasMore: false };
|
|
336
|
+
|
|
337
|
+
const groupedFiles = new Map<string, string[]>();
|
|
338
|
+
const filePattern = /^([A-Za-z0-9_-]+)_\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
339
|
+
for (const file of fs.readdirSync(dir)) {
|
|
340
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
341
|
+
const match = file.match(filePattern);
|
|
342
|
+
if (!match?.[1]) continue;
|
|
343
|
+
const historySessionId = match[1];
|
|
344
|
+
const files = groupedFiles.get(historySessionId) || [];
|
|
345
|
+
files.push(file);
|
|
346
|
+
groupedFiles.set(historySessionId, files);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const summaries: SavedHistorySessionSummary[] = [];
|
|
350
|
+
for (const [historySessionId, files] of groupedFiles.entries()) {
|
|
351
|
+
let messageCount = 0;
|
|
352
|
+
let firstMessageAt = 0;
|
|
353
|
+
let lastMessageAt = 0;
|
|
354
|
+
let sessionTitle = '';
|
|
355
|
+
let preview = '';
|
|
356
|
+
|
|
357
|
+
for (const file of files.sort()) {
|
|
358
|
+
const filePath = path.join(dir, file);
|
|
359
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
360
|
+
const lines = content.split('\n').filter(Boolean);
|
|
361
|
+
for (const line of lines) {
|
|
362
|
+
let parsed: HistoryMessage | null = null;
|
|
363
|
+
try {
|
|
364
|
+
parsed = JSON.parse(line) as HistoryMessage;
|
|
365
|
+
} catch {
|
|
366
|
+
parsed = null;
|
|
367
|
+
}
|
|
368
|
+
if (!parsed || parsed.historySessionId !== historySessionId) continue;
|
|
369
|
+
messageCount += 1;
|
|
370
|
+
if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
|
|
371
|
+
if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
|
|
372
|
+
if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
|
|
373
|
+
if (parsed.role !== 'system' && parsed.content.trim()) preview = parsed.content.trim();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (messageCount === 0 || !lastMessageAt) continue;
|
|
378
|
+
summaries.push({
|
|
379
|
+
historySessionId,
|
|
380
|
+
sessionTitle: sessionTitle || undefined,
|
|
381
|
+
messageCount,
|
|
382
|
+
firstMessageAt,
|
|
383
|
+
lastMessageAt,
|
|
384
|
+
preview: preview || undefined,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
summaries.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
389
|
+
const offset = Math.max(0, options.offset || 0);
|
|
390
|
+
const limit = Math.max(1, options.limit || 30);
|
|
391
|
+
const sliced = summaries.slice(offset, offset + limit);
|
|
392
|
+
return {
|
|
393
|
+
sessions: sliced,
|
|
394
|
+
hasMore: summaries.length > offset + limit,
|
|
395
|
+
};
|
|
396
|
+
} catch {
|
|
397
|
+
return { sessions: [], hasMore: false };
|
|
398
|
+
}
|
|
399
|
+
}
|
package/src/config/config.d.ts
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { WorkspaceEntry } from './workspaces.js';
|
|
7
7
|
import type { RecentActivityEntry } from './recent-activity.js';
|
|
8
|
+
import type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
8
9
|
export type { WorkspaceEntry } from './workspaces.js';
|
|
9
10
|
export type { RecentActivityEntry } from './recent-activity.js';
|
|
11
|
+
export type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
10
12
|
export interface ADHDevConfig {
|
|
11
13
|
serverUrl: string;
|
|
12
14
|
selectedIde: string | null;
|
|
@@ -23,6 +25,8 @@ export interface ADHDevConfig {
|
|
|
23
25
|
defaultWorkspaceId?: string | null;
|
|
24
26
|
/** Unified recent activity across IDE / CLI / ACP launch flows */
|
|
25
27
|
recentActivity?: RecentActivityEntry[];
|
|
28
|
+
/** Persistent resume-capable provider sessions keyed by providerSessionId */
|
|
29
|
+
savedProviderSessions?: SavedProviderSessionEntry[];
|
|
26
30
|
/** Last seen timestamps for live sessions, keyed by sessionId */
|
|
27
31
|
sessionReads?: Record<string, number>;
|
|
28
32
|
/** Last seen completion marker for live sessions, keyed by sessionId */
|
package/src/config/config.ts
CHANGED
|
@@ -10,8 +10,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'f
|
|
|
10
10
|
import { randomUUID } from 'crypto';
|
|
11
11
|
import type { WorkspaceEntry } from './workspaces.js';
|
|
12
12
|
import type { RecentActivityEntry } from './recent-activity.js';
|
|
13
|
+
import type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
13
14
|
export type { WorkspaceEntry } from './workspaces.js';
|
|
14
15
|
export type { RecentActivityEntry } from './recent-activity.js';
|
|
16
|
+
export type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
15
17
|
|
|
16
18
|
export interface ADHDevConfig {
|
|
17
19
|
// Server connection
|
|
@@ -44,6 +46,8 @@ export interface ADHDevConfig {
|
|
|
44
46
|
|
|
45
47
|
/** Unified recent activity across IDE / CLI / ACP launch flows */
|
|
46
48
|
recentActivity?: RecentActivityEntry[];
|
|
49
|
+
/** Persistent resume-capable provider sessions keyed by providerSessionId */
|
|
50
|
+
savedProviderSessions?: SavedProviderSessionEntry[];
|
|
47
51
|
/** Last seen timestamps for live sessions, keyed by sessionId */
|
|
48
52
|
sessionReads?: Record<string, number>;
|
|
49
53
|
/** Last seen completion marker for live sessions, keyed by sessionId */
|
|
@@ -65,7 +69,7 @@ export interface ADHDevConfig {
|
|
|
65
69
|
* Server-side D1 `machines.id` — the row ID assigned when daemon registers via
|
|
66
70
|
* `POST /cli/complete`. Used as fallback for machine lookup on re-auth.
|
|
67
71
|
*
|
|
68
|
-
* @deprecated Legacy bridge field — will be removed after 2026-
|
|
72
|
+
* @deprecated Legacy bridge field — will be removed after 2026-05-01.
|
|
69
73
|
* Modern auth flow uses `machineSecret` (adm_) to identify machines.
|
|
70
74
|
*/
|
|
71
75
|
registeredMachineId?: string;
|
|
@@ -99,6 +103,7 @@ const DEFAULT_CONFIG: ADHDevConfig = {
|
|
|
99
103
|
workspaces: [],
|
|
100
104
|
defaultWorkspaceId: null,
|
|
101
105
|
recentActivity: [],
|
|
106
|
+
savedProviderSessions: [],
|
|
102
107
|
sessionReads: {},
|
|
103
108
|
sessionReadMarkers: {},
|
|
104
109
|
machineNickname: null,
|
|
@@ -161,6 +166,7 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
|
|
|
161
166
|
workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces as WorkspaceEntry[] : [],
|
|
162
167
|
defaultWorkspaceId: asNullableString(parsed.defaultWorkspaceId) ?? asNullableString(parsed.activeWorkspaceId),
|
|
163
168
|
recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
|
|
169
|
+
savedProviderSessions: Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions as SavedProviderSessionEntry[] : [],
|
|
164
170
|
sessionReads: mergedSessionReads,
|
|
165
171
|
sessionReadMarkers,
|
|
166
172
|
machineNickname: asNullableString(parsed.machineNickname),
|
|
@@ -187,7 +193,7 @@ function ensureMachineId(config: ADHDevConfig): { config: ADHDevConfig; changed:
|
|
|
187
193
|
return { config, changed: false };
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
// TODO(2026-
|
|
196
|
+
// TODO(2026-05-01): Remove this legacy bridge after cloud clients have had
|
|
191
197
|
// time to persist registeredMachineId from the upgraded setup/login flow.
|
|
192
198
|
const legacyRegisteredMachineId = (!config.registeredMachineId && config.machineSecret && config.machineId)
|
|
193
199
|
? config.machineId
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Unlike live session state, this is launch oriented:
|
|
5
5
|
* - one normalized row shape for IDE / CLI / ACP
|
|
6
|
-
* - deduped by kind + providerType + workspace
|
|
6
|
+
* - deduped by provider session when available, else by kind + providerType + workspace
|
|
7
7
|
* - used only for quick-launch shortcuts
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -16,6 +16,7 @@ export interface RecentActivityEntry {
|
|
|
16
16
|
kind: 'ide' | 'cli' | 'acp';
|
|
17
17
|
providerType: string;
|
|
18
18
|
providerName: string;
|
|
19
|
+
providerSessionId?: string;
|
|
19
20
|
workspace?: string | null;
|
|
20
21
|
currentModel?: string;
|
|
21
22
|
title?: string;
|
|
@@ -37,6 +38,16 @@ export function buildRecentActivityKey(entry: Pick<RecentActivityEntry, 'kind' |
|
|
|
37
38
|
return `${entry.kind}:${entry.providerType}:${normalizeWorkspace(entry.workspace)}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export function buildRecentActivityKeyForEntry(
|
|
42
|
+
entry: Pick<RecentActivityEntry, 'kind' | 'providerType' | 'workspace' | 'providerSessionId'>,
|
|
43
|
+
) {
|
|
44
|
+
const providerSessionId = typeof entry.providerSessionId === 'string' ? entry.providerSessionId.trim() : '';
|
|
45
|
+
if (providerSessionId) {
|
|
46
|
+
return `${entry.kind}:${entry.providerType}:session:${providerSessionId}`;
|
|
47
|
+
}
|
|
48
|
+
return buildRecentActivityKey(entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
export function appendRecentActivity(
|
|
41
52
|
config: ADHDevConfig,
|
|
42
53
|
entry: Omit<RecentActivityEntry, 'id' | 'lastUsedAt'> & { lastUsedAt?: number },
|
|
@@ -44,7 +55,7 @@ export function appendRecentActivity(
|
|
|
44
55
|
const nextEntry: RecentActivityEntry = {
|
|
45
56
|
...entry,
|
|
46
57
|
workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
|
|
47
|
-
id:
|
|
58
|
+
id: buildRecentActivityKeyForEntry(entry),
|
|
48
59
|
lastUsedAt: entry.lastUsedAt || Date.now(),
|
|
49
60
|
};
|
|
50
61
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import type { ADHDevConfig } from './config.js';
|
|
3
|
+
import { expandPath } from './workspaces.js';
|
|
4
|
+
|
|
5
|
+
export interface SavedProviderSessionEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: 'cli' | 'acp';
|
|
8
|
+
providerType: string;
|
|
9
|
+
providerName: string;
|
|
10
|
+
providerSessionId: string;
|
|
11
|
+
workspace?: string | null;
|
|
12
|
+
currentModel?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
lastUsedAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_SAVED_SESSIONS = 500;
|
|
19
|
+
|
|
20
|
+
function normalizeWorkspace(workspace?: string | null) {
|
|
21
|
+
if (!workspace) return '';
|
|
22
|
+
try {
|
|
23
|
+
return path.resolve(expandPath(workspace));
|
|
24
|
+
} catch {
|
|
25
|
+
return path.resolve(workspace);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildSavedProviderSessionKey(providerSessionId: string) {
|
|
30
|
+
return `saved:${providerSessionId.trim()}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function upsertSavedProviderSession(
|
|
34
|
+
config: ADHDevConfig,
|
|
35
|
+
entry: Omit<SavedProviderSessionEntry, 'id' | 'createdAt' | 'lastUsedAt'> & { createdAt?: number; lastUsedAt?: number },
|
|
36
|
+
): ADHDevConfig {
|
|
37
|
+
const providerSessionId = typeof entry.providerSessionId === 'string' ? entry.providerSessionId.trim() : '';
|
|
38
|
+
if (!providerSessionId) return config;
|
|
39
|
+
|
|
40
|
+
const id = buildSavedProviderSessionKey(providerSessionId);
|
|
41
|
+
const existing = (config.savedProviderSessions || []).find(item => item.id === id);
|
|
42
|
+
const nextEntry: SavedProviderSessionEntry = {
|
|
43
|
+
id,
|
|
44
|
+
kind: entry.kind,
|
|
45
|
+
providerType: entry.providerType,
|
|
46
|
+
providerName: entry.providerName,
|
|
47
|
+
providerSessionId,
|
|
48
|
+
workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
|
|
49
|
+
currentModel: entry.currentModel,
|
|
50
|
+
title: entry.title,
|
|
51
|
+
createdAt: existing?.createdAt || entry.createdAt || Date.now(),
|
|
52
|
+
lastUsedAt: entry.lastUsedAt || Date.now(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const filtered = (config.savedProviderSessions || []).filter(item => item.id !== id);
|
|
56
|
+
return {
|
|
57
|
+
...config,
|
|
58
|
+
savedProviderSessions: [nextEntry, ...filtered].slice(0, MAX_SAVED_SESSIONS),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getSavedProviderSessions(
|
|
63
|
+
config: ADHDevConfig,
|
|
64
|
+
filters?: { providerType?: string; kind?: SavedProviderSessionEntry['kind'] },
|
|
65
|
+
): SavedProviderSessionEntry[] {
|
|
66
|
+
return [...(config.savedProviderSessions || [])]
|
|
67
|
+
.filter(entry => {
|
|
68
|
+
if (filters?.providerType && entry.providerType !== filters.providerType) return false;
|
|
69
|
+
if (filters?.kind && entry.kind !== filters.kind) return false;
|
|
70
|
+
return true;
|
|
71
|
+
})
|
|
72
|
+
.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
73
|
+
}
|