@adhdev/daemon-core 0.7.44 → 0.7.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +1 -0
  2. package/dist/cli-adapters/pty-transport.d.ts +1 -0
  3. package/dist/commands/cli-manager.d.ts +11 -2
  4. package/dist/config/chat-history.d.ts +29 -2
  5. package/dist/config/config.d.ts +4 -0
  6. package/dist/config/recent-activity.d.ts +3 -1
  7. package/dist/config/saved-sessions.d.ts +22 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +4623 -3973
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +4617 -3969
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/providers/cli-provider-instance.d.ts +24 -1
  14. package/dist/providers/contracts.d.ts +3 -0
  15. package/dist/providers/provider-instance.d.ts +1 -0
  16. package/dist/shared-types.d.ts +2 -0
  17. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
  18. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
  19. package/node_modules/@adhdev/session-host-core/dist/index.js +11 -2
  20. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  21. package/node_modules/@adhdev/session-host-core/dist/index.mjs +11 -2
  22. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  23. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  24. package/package.json +1 -1
  25. package/src/boot/daemon-lifecycle.ts +19 -15
  26. package/src/cli-adapters/provider-cli-adapter.ts +11 -5
  27. package/src/cli-adapters/pty-transport.ts +1 -0
  28. package/src/cli-adapters/session-host-transport.ts +19 -0
  29. package/src/commands/chat-commands.ts +28 -8
  30. package/src/commands/cli-manager.ts +259 -22
  31. package/src/commands/router.ts +52 -1
  32. package/src/config/chat-history.ts +193 -10
  33. package/src/config/config.d.ts +4 -0
  34. package/src/config/config.ts +6 -0
  35. package/src/config/recent-activity.ts +13 -2
  36. package/src/config/saved-sessions.ts +73 -0
  37. package/src/daemon/dev-auto-implement.ts +23 -5
  38. package/src/daemon/dev-server.ts +22 -4
  39. package/src/index.ts +2 -0
  40. package/src/providers/cli-provider-instance.ts +205 -4
  41. package/src/providers/contracts.ts +3 -0
  42. package/src/providers/provider-instance.d.ts +1 -0
  43. package/src/providers/provider-instance.ts +1 -0
  44. package/src/session-host/runtime-support.ts +1 -0
  45. package/src/shared-types.d.ts +2 -0
  46. package/src/shared-types.ts +2 -0
  47. package/src/status/builders.ts +1 -0
  48. package/src/status/snapshot.ts +1 -0
@@ -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,22 @@ interface HistoryMessage {
21
21
  receivedAt: number; // epoch ms
22
22
  role: 'user' | 'assistant' | 'system';
23
23
  content: string;
24
+ kind?: string;
24
25
  agent: string; // e.g. 'antigravity', 'cursor', 'gemini-cli'
25
26
  instanceId?: string; // IDE instance UUID (distinguishes windows of the same agent type)
27
+ historySessionId?: string; // Persistent provider-side conversation/session key
26
28
  sessionTitle?: string;
27
29
  }
28
30
 
31
+ export interface SavedHistorySessionSummary {
32
+ historySessionId: string;
33
+ sessionTitle?: string;
34
+ messageCount: number;
35
+ firstMessageAt: number;
36
+ lastMessageAt: number;
37
+ preview?: string;
38
+ }
39
+
29
40
  export class ChatHistoryWriter {
30
41
  /** Last seen message count per agent (deduplication) */
31
42
  private lastSeenCounts = new Map<string, number>();
@@ -43,15 +54,17 @@ export class ChatHistoryWriter {
43
54
  */
44
55
  appendNewMessages(
45
56
  agentType: string,
46
- messages: Array<{ role: string; content: string; receivedAt?: number }>,
57
+ messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; historyDedupKey?: string }>,
47
58
  sessionTitle?: string,
48
59
  instanceId?: string,
60
+ historySessionId?: string,
49
61
  ): void {
50
62
  if (!messages || messages.length === 0) return;
51
63
 
52
64
  try {
53
- // dedup key: agentType + instanceId
54
- const dedupKey = instanceId ? `${agentType}:${instanceId}` : agentType;
65
+ // dedup key: agentType + persistent history key (fallback: runtime instanceId)
66
+ const effectiveHistoryKey = historySessionId || instanceId;
67
+ const dedupKey = effectiveHistoryKey ? `${agentType}:${effectiveHistoryKey}` : agentType;
55
68
  let seenHashes = this.lastSeenHashes.get(dedupKey);
56
69
  if (!seenHashes) {
57
70
  seenHashes = new Set<string>();
@@ -61,7 +74,7 @@ export class ChatHistoryWriter {
61
74
  // Filter new messages
62
75
  const newMessages: HistoryMessage[] = [];
63
76
  for (const msg of messages) {
64
- const hash = `${msg.role}:${(msg.content || '').slice(0, 50)}`;
77
+ const hash = msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`;
65
78
  if (seenHashes.has(hash)) continue;
66
79
  seenHashes.add(hash);
67
80
  newMessages.push({
@@ -69,20 +82,22 @@ export class ChatHistoryWriter {
69
82
  receivedAt: msg.receivedAt || Date.now(),
70
83
  role: msg.role as 'user' | 'assistant' | 'system',
71
84
  content: msg.content || '',
85
+ kind: typeof msg.kind === 'string' ? msg.kind : undefined,
72
86
  agent: agentType,
73
87
  instanceId,
88
+ historySessionId: effectiveHistoryKey,
74
89
  sessionTitle,
75
90
  });
76
91
  }
77
92
 
78
93
  if (newMessages.length === 0) return;
79
94
 
80
- // Append to file — separate file if instanceId exists
95
+ // Append to file — keyed by persistent history session when available
81
96
  const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
82
97
  fs.mkdirSync(dir, { recursive: true });
83
98
 
84
99
  const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
85
- const filePrefix = instanceId ? `${this.sanitize(instanceId)}_` : '';
100
+ const filePrefix = effectiveHistoryKey ? `${this.sanitize(effectiveHistoryKey)}_` : '';
86
101
  const filePath = path.join(dir, `${filePrefix}${date}.jsonl`);
87
102
  const lines = newMessages.map(m => JSON.stringify(m)).join('\n') + '\n';
88
103
  fs.appendFileSync(filePath, lines, 'utf-8');
@@ -92,7 +107,7 @@ export class ChatHistoryWriter {
92
107
  if (messages.length < prevCount * 0.5 && prevCount > 3) {
93
108
  seenHashes.clear();
94
109
  for (const msg of messages) {
95
- seenHashes.add(`${msg.role}:${(msg.content || '').slice(0, 50)}`);
110
+ seenHashes.add(msg.historyDedupKey || `${msg.kind || 'standard'}:${msg.role}:${(msg.content || '').slice(0, 50)}`);
96
111
  }
97
112
  }
98
113
  this.lastSeenCounts.set(dedupKey, messages.length);
@@ -107,6 +122,101 @@ export class ChatHistoryWriter {
107
122
  }
108
123
  }
109
124
 
125
+ appendSystemMarker(
126
+ agentType: string,
127
+ content: string,
128
+ options: {
129
+ sessionTitle?: string;
130
+ instanceId?: string;
131
+ historySessionId?: string;
132
+ dedupKey?: string;
133
+ receivedAt?: number;
134
+ } = {},
135
+ ): void {
136
+ this.appendNewMessages(
137
+ agentType,
138
+ [{
139
+ role: 'system',
140
+ kind: 'system',
141
+ content,
142
+ receivedAt: options.receivedAt,
143
+ historyDedupKey: options.dedupKey,
144
+ }],
145
+ options.sessionTitle,
146
+ options.instanceId,
147
+ options.historySessionId,
148
+ );
149
+ }
150
+
151
+ promoteHistorySession(
152
+ agentType: string,
153
+ previousHistorySessionId: string,
154
+ nextHistorySessionId: string,
155
+ ): void {
156
+ const fromId = String(previousHistorySessionId || '').trim();
157
+ const toId = String(nextHistorySessionId || '').trim();
158
+ if (!fromId || !toId || fromId === toId) return;
159
+
160
+ try {
161
+ const fromDedupKey = `${agentType}:${fromId}`;
162
+ const toDedupKey = `${agentType}:${toId}`;
163
+ const fromHashes = this.lastSeenHashes.get(fromDedupKey);
164
+ if (fromHashes?.size) {
165
+ const nextHashes = this.lastSeenHashes.get(toDedupKey) || new Set<string>();
166
+ for (const hash of fromHashes) nextHashes.add(hash);
167
+ this.lastSeenHashes.set(toDedupKey, nextHashes);
168
+ this.lastSeenHashes.delete(fromDedupKey);
169
+ }
170
+ const fromCount = this.lastSeenCounts.get(fromDedupKey);
171
+ if (typeof fromCount === 'number') {
172
+ this.lastSeenCounts.set(toDedupKey, Math.max(fromCount, this.lastSeenCounts.get(toDedupKey) || 0));
173
+ this.lastSeenCounts.delete(fromDedupKey);
174
+ }
175
+
176
+ const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
177
+ if (!fs.existsSync(dir)) return;
178
+
179
+ const fromPrefix = `${this.sanitize(fromId)}_`;
180
+ const toPrefix = `${this.sanitize(toId)}_`;
181
+ const files = fs.readdirSync(dir).filter((file) => file.startsWith(fromPrefix) && file.endsWith('.jsonl'));
182
+
183
+ for (const file of files) {
184
+ const sourcePath = path.join(dir, file);
185
+ const targetPath = path.join(dir, `${toPrefix}${file.slice(fromPrefix.length)}`);
186
+ const sourceLines = fs.readFileSync(sourcePath, 'utf-8').split('\n').filter(Boolean);
187
+ const rewritten = sourceLines
188
+ .map((line) => {
189
+ try {
190
+ const parsed = JSON.parse(line) as HistoryMessage;
191
+ if (parsed.historySessionId !== fromId) return null;
192
+ return JSON.stringify({
193
+ ...parsed,
194
+ historySessionId: toId,
195
+ });
196
+ } catch {
197
+ return null;
198
+ }
199
+ })
200
+ .filter((line): line is string => !!line);
201
+ if (rewritten.length === 0) {
202
+ fs.unlinkSync(sourcePath);
203
+ continue;
204
+ }
205
+
206
+ const existing = fs.existsSync(targetPath)
207
+ ? new Set(fs.readFileSync(targetPath, 'utf-8').split('\n').filter(Boolean))
208
+ : new Set<string>();
209
+ const nextLines = rewritten.filter((line) => !existing.has(line));
210
+ if (nextLines.length > 0) {
211
+ fs.appendFileSync(targetPath, `${nextLines.join('\n')}\n`, 'utf-8');
212
+ }
213
+ fs.unlinkSync(sourcePath);
214
+ }
215
+ } catch {
216
+ // Ignore promotion failure; future messages will still write to the new session key.
217
+ }
218
+ }
219
+
110
220
  /** Called when agent session is explicitly changed */
111
221
  onSessionChange(agentType: string): void {
112
222
  this.lastSeenHashes.delete(agentType);
@@ -157,15 +267,15 @@ export function readChatHistory(
157
267
  agentType: string,
158
268
  offset: number = 0,
159
269
  limit: number = 30,
160
- instanceId?: string,
270
+ historySessionId?: string,
161
271
  ): { messages: HistoryMessage[]; hasMore: boolean } {
162
272
  try {
163
273
  const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
164
274
  const dir = path.join(HISTORY_DIR, sanitized);
165
275
  if (!fs.existsSync(dir)) return { messages: [], hasMore: false };
166
276
 
167
- // JSONL file list — filter by instanceId prefix if specified
168
- const sanitizedInstance = instanceId?.replace(/[^a-zA-Z0-9_-]/g, '_');
277
+ // JSONL file list — filter by persistent history key when specified
278
+ const sanitizedInstance = historySessionId?.replace(/[^a-zA-Z0-9_-]/g, '_');
169
279
  const files = fs.readdirSync(dir)
170
280
  .filter(f => {
171
281
  if (!f.endsWith('.jsonl')) return false;
@@ -210,3 +320,76 @@ export function readChatHistory(
210
320
  return { messages: [], hasMore: false };
211
321
  }
212
322
  }
323
+
324
+ export function listSavedHistorySessions(
325
+ agentType: string,
326
+ options: { offset?: number; limit?: number } = {},
327
+ ): { sessions: SavedHistorySessionSummary[]; hasMore: boolean } {
328
+ try {
329
+ const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
330
+ const dir = path.join(HISTORY_DIR, sanitized);
331
+ if (!fs.existsSync(dir)) return { sessions: [], hasMore: false };
332
+
333
+ const groupedFiles = new Map<string, string[]>();
334
+ const filePattern = /^([A-Za-z0-9_-]+)_\d{4}-\d{2}-\d{2}\.jsonl$/;
335
+ for (const file of fs.readdirSync(dir)) {
336
+ if (!file.endsWith('.jsonl')) continue;
337
+ const match = file.match(filePattern);
338
+ if (!match?.[1]) continue;
339
+ const historySessionId = match[1];
340
+ const files = groupedFiles.get(historySessionId) || [];
341
+ files.push(file);
342
+ groupedFiles.set(historySessionId, files);
343
+ }
344
+
345
+ const summaries: SavedHistorySessionSummary[] = [];
346
+ for (const [historySessionId, files] of groupedFiles.entries()) {
347
+ let messageCount = 0;
348
+ let firstMessageAt = 0;
349
+ let lastMessageAt = 0;
350
+ let sessionTitle = '';
351
+ let preview = '';
352
+
353
+ for (const file of files.sort()) {
354
+ const filePath = path.join(dir, file);
355
+ const content = fs.readFileSync(filePath, 'utf-8');
356
+ const lines = content.split('\n').filter(Boolean);
357
+ for (const line of lines) {
358
+ let parsed: HistoryMessage | null = null;
359
+ try {
360
+ parsed = JSON.parse(line) as HistoryMessage;
361
+ } catch {
362
+ parsed = null;
363
+ }
364
+ if (!parsed || parsed.historySessionId !== historySessionId) continue;
365
+ messageCount += 1;
366
+ if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
367
+ if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
368
+ if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
369
+ if (parsed.role !== 'system' && parsed.content.trim()) preview = parsed.content.trim();
370
+ }
371
+ }
372
+
373
+ if (messageCount === 0 || !lastMessageAt) continue;
374
+ summaries.push({
375
+ historySessionId,
376
+ sessionTitle: sessionTitle || undefined,
377
+ messageCount,
378
+ firstMessageAt,
379
+ lastMessageAt,
380
+ preview: preview || undefined,
381
+ });
382
+ }
383
+
384
+ summaries.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
385
+ const offset = Math.max(0, options.offset || 0);
386
+ const limit = Math.max(1, options.limit || 30);
387
+ const sliced = summaries.slice(offset, offset + limit);
388
+ return {
389
+ sessions: sliced,
390
+ hasMore: summaries.length > offset + limit,
391
+ };
392
+ } catch {
393
+ return { sessions: [], hasMore: false };
394
+ }
395
+ }
@@ -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 */
@@ -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 */
@@ -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),
@@ -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: buildRecentActivityKey(entry),
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
+ }
@@ -952,22 +952,32 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
952
952
  lines.push('| `detectStatus` | `{ tail, screenText, rawBuffer }` | `idle`, `generating`, `waiting_approval`, or `error` |');
953
953
  lines.push('| `parseApproval` | `{ buffer, rawBuffer, tail }` | `{ message, buttons }` or `null` |');
954
954
  lines.push('');
955
+ lines.push('## Primary Source of Truth');
956
+ lines.push('The runtime now provides a reliable current-screen snapshot. Treat `screenText` as the primary source of truth for the LIVE visible UI.');
957
+ lines.push('That means:');
958
+ lines.push('- Use `screenText` first for prompt detection, approval UI, status, and visible assistant content.');
959
+ lines.push('- Use `rawBuffer` only as supporting evidence when ANSI/style/cursor cues matter.');
960
+ lines.push('- Use `buffer` only when the visible screen does not contain enough text to recover the latest assistant answer.');
961
+ lines.push('- Do NOT build the parser around stale transcript noise if the current screen already gives the answer.');
962
+ lines.push('');
955
963
 
956
964
  lines.push('## Rules');
957
965
  lines.push('0. **🚫 SCOPE CONSTRAINT**: You may ONLY edit files marked ✏️ EDIT above. ALL other files are READ-ONLY. Do NOT modify, rewrite, refactor, or "improve" any file not explicitly marked as editable — even if you notice bugs or improvements. No exceptions.');
958
966
  lines.push('1. These scripts run in Node.js CommonJS, not in the browser. Do NOT use DOM APIs.');
959
- lines.push('2. Prefer `screenText` for current visible UI state. That is the PTY equivalent of parsing the current IDE DOM.');
967
+ lines.push('2. Prefer `screenText` for current visible UI state. It is now the PTY equivalent of a trustworthy live DOM snapshot.');
960
968
  lines.push('3. Use `messages` as prior transcript state so redraws do not duplicate old turns on every parse.');
961
969
  lines.push('4. Use `partialResponse` for the actively streaming assistant text when status is `generating`.');
962
- lines.push('5. `detectStatus` must stay lightweight and tail-based. Do not scan the entire history there.');
963
- lines.push('6. `parseApproval` should understand the live approval area and return clean button labels.');
964
- lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts matter. Do not depend on raw escape noise unless necessary.');
970
+ lines.push('5. `detectStatus` must stay lightweight and current-screen-oriented. Prefer the active bottom-of-screen region over stale history.');
971
+ lines.push('6. `parseApproval` should understand the live approval area and return clean button labels from the CURRENT visible modal.');
972
+ lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts or style cues matter. Do not depend on raw escape noise unless necessary.');
965
973
  lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
966
974
  lines.push('9. Do NOT modify ANY file not explicitly marked ✏️ EDIT above. No exceptions — no "tiny supporting changes" to other files.');
967
975
  lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
968
976
  lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
969
977
  lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
970
978
  lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
979
+ lines.push('14. If the visible current screen is clean and sufficient, do NOT fall back to complex buffer heuristics. Simpler current-screen parsing is preferred.');
980
+ lines.push('15. Before changing parser logic, verify whether `provider.json` submit/approval behavior (`sendDelayMs`, `approvalKeys`, submit strategy) is the simpler and more correct fix.');
971
981
  lines.push('');
972
982
 
973
983
  lines.push('## Task');
@@ -990,6 +1000,12 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
990
1000
  lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/status`);
991
1001
  lines.push('```');
992
1002
  lines.push('');
1003
+ lines.push('The debug payload should be read in this priority order:');
1004
+ lines.push('1. `screenText` / current visible state');
1005
+ lines.push('2. parsed `status`, `messages`, `activeModal`');
1006
+ lines.push('3. `rawBuffer` only for style/control-sequence cues');
1007
+ lines.push('4. `buffer` only when the current screen is insufficient');
1008
+ lines.push('');
993
1009
  lines.push('Extract the current `instanceId` from the launch or status response and keep using it below.');
994
1010
  lines.push('');
995
1011
  lines.push('### 3. Send a realistic approval-triggering prompt');
@@ -1036,6 +1052,8 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
1036
1052
  lines.push('5. Confirm the Python file was actually created and executed, not just described in chat text.');
1037
1053
  lines.push('6. Confirm the final assistant transcript includes the exact Python output, including the working directory line and the five square numbers.');
1038
1054
  lines.push('7. Re-run the debug endpoints after edits. Do NOT finish until the parsed result looks correct.');
1055
+ lines.push('8. Confirm the parser still works after a redraw or scroll change without duplicating transcript history.');
1056
+ lines.push('9. Confirm the implementation prefers current-screen signals over stale history when both are present.');
1039
1057
  lines.push('');
1040
1058
 
1041
1059
  if (userComment) {
@@ -1091,4 +1109,4 @@ export function sendAutoImplSSE(ctx: DevServerContext, msg: { event: string; dat
1091
1109
  for (const client of ctx.autoImplSSEClients) {
1092
1110
  try { client.write(payload); } catch { /* ignore */ }
1093
1111
  }
1094
- }
1112
+ }
@@ -1427,22 +1427,32 @@ export class DevServer implements DevServerContext {
1427
1427
  lines.push('| `detectStatus` | `{ tail, screenText, rawBuffer }` | `idle`, `generating`, `waiting_approval`, or `error` |');
1428
1428
  lines.push('| `parseApproval` | `{ buffer, rawBuffer, tail }` | `{ message, buttons }` or `null` |');
1429
1429
  lines.push('');
1430
+ lines.push('## Primary Source of Truth');
1431
+ lines.push('The runtime now provides a reliable current-screen snapshot. Treat `screenText` as the primary source of truth for the LIVE visible UI.');
1432
+ lines.push('That means:');
1433
+ lines.push('- Use `screenText` first for prompt detection, approval UI, status, and visible assistant content.');
1434
+ lines.push('- Use `rawBuffer` only as supporting evidence when ANSI/style/cursor cues matter.');
1435
+ lines.push('- Use `buffer` only when the visible screen does not contain enough text to recover the latest assistant answer.');
1436
+ lines.push('- Do NOT build the parser around stale transcript noise if the current screen already gives the answer.');
1437
+ lines.push('');
1430
1438
 
1431
1439
  lines.push('## Rules');
1432
1440
  lines.push('0. **🚫 SCOPE CONSTRAINT**: You may ONLY edit files marked ✏️ EDIT above. ALL other files are READ-ONLY. Do NOT modify, rewrite, refactor, or "improve" any file not explicitly marked as editable — even if you notice bugs or improvements. No exceptions.');
1433
1441
  lines.push('1. These scripts run in Node.js CommonJS, not in the browser. Do NOT use DOM APIs.');
1434
- lines.push('2. Prefer `screenText` for current visible UI state. That is the PTY equivalent of parsing the current IDE DOM.');
1442
+ lines.push('2. Prefer `screenText` for current visible UI state. It is now the PTY equivalent of a trustworthy live DOM snapshot.');
1435
1443
  lines.push('3. Use `messages` as prior transcript state so redraws do not duplicate old turns on every parse.');
1436
1444
  lines.push('4. Use `partialResponse` for the actively streaming assistant text when status is `generating`.');
1437
- lines.push('5. `detectStatus` must stay lightweight and tail-based. Do not scan the entire history there.');
1438
- lines.push('6. `parseApproval` should understand the live approval area and return clean button labels.');
1439
- lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts matter. Do not depend on raw escape noise unless necessary.');
1445
+ lines.push('5. `detectStatus` must stay lightweight and current-screen-oriented. Prefer the active bottom-of-screen region over stale history.');
1446
+ lines.push('6. `parseApproval` should understand the live approval area and return clean button labels from the CURRENT visible modal.');
1447
+ lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts or style cues matter. Do not depend on raw escape noise unless necessary.');
1440
1448
  lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
1441
1449
  lines.push('9. Do NOT modify ANY file not explicitly marked ✏️ EDIT above. No exceptions — no "tiny supporting changes" to other files.');
1442
1450
  lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
1443
1451
  lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
1444
1452
  lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
1445
1453
  lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
1454
+ lines.push('14. If the visible current screen is clean and sufficient, do NOT fall back to complex buffer heuristics. Simpler current-screen parsing is preferred.');
1455
+ lines.push('15. Before changing parser logic, verify whether `provider.json` submit/approval behavior (`sendDelayMs`, `approvalKeys`, submit strategy) is the simpler and more correct fix.');
1446
1456
  lines.push('');
1447
1457
 
1448
1458
  lines.push('## Task');
@@ -1465,6 +1475,12 @@ export class DevServer implements DevServerContext {
1465
1475
  lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/status`);
1466
1476
  lines.push('```');
1467
1477
  lines.push('');
1478
+ lines.push('The debug payload should be read in this priority order:');
1479
+ lines.push('1. `screenText` / current visible state');
1480
+ lines.push('2. parsed `status`, `messages`, `activeModal`');
1481
+ lines.push('3. `rawBuffer` only for style/control-sequence cues');
1482
+ lines.push('4. `buffer` only when the current screen is insufficient');
1483
+ lines.push('');
1468
1484
  lines.push('Extract the current `instanceId` from the launch or status response and keep using it below.');
1469
1485
  lines.push('');
1470
1486
  lines.push('### 3. Send a realistic approval-triggering prompt');
@@ -1511,6 +1527,8 @@ export class DevServer implements DevServerContext {
1511
1527
  lines.push('5. Confirm the Python file was actually created and executed, not just described in chat text.');
1512
1528
  lines.push('6. Confirm the final assistant transcript includes the exact Python output, including the working directory line and the five square numbers.');
1513
1529
  lines.push('7. Re-run the debug endpoints after edits. Do NOT finish until the parsed result looks correct.');
1530
+ lines.push('8. Confirm the parser still works after a redraw or scroll change without duplicating transcript history.');
1531
+ lines.push('9. Confirm the implementation prefers current-screen signals over stale history when both are present.');
1514
1532
  lines.push('');
1515
1533
 
1516
1534
  if (userComment) {