@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
package/src/index.ts CHANGED
@@ -66,6 +66,8 @@ export { loadConfig, saveConfig, resetConfig, isSetupComplete, markSetupComplete
66
66
  export { getWorkspaceState } from './config/workspaces.js';
67
67
  export { appendRecentActivity, getRecentActivity } from './config/recent-activity.js';
68
68
  export type { RecentActivityEntry } from './config/recent-activity.js';
69
+ export { getSavedProviderSessions, upsertSavedProviderSession } from './config/saved-sessions.js';
70
+ export type { SavedProviderSessionEntry } from './config/saved-sessions.js';
69
71
 
70
72
  // ── Detection ──
71
73
  export { detectIDEs } from './detection/ide-detector.js';
@@ -5,8 +5,11 @@
5
5
  * collectCliData() + status transition logic from daemon-status.ts moved here.
6
6
  */
7
7
 
8
+ import * as os from 'os';
8
9
  import * as path from 'path';
9
10
  import * as crypto from 'crypto';
11
+ import * as fs from 'fs';
12
+ import { createRequire } from 'node:module';
10
13
  import type { ProviderModule } from './contracts.js';
11
14
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
12
15
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
@@ -16,6 +19,26 @@ import { StatusMonitor } from './status-monitor.js';
16
19
  import { ChatHistoryWriter } from '../config/chat-history.js';
17
20
  import { LOG } from '../logging/logger.js';
18
21
 
22
+ let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
23
+ prepare(sql: string): { get(...params: Array<string | number>): unknown };
24
+ close(): void;
25
+ }) | null = null;
26
+
27
+ function getDatabaseSync() {
28
+ if (CachedDatabaseSync) return CachedDatabaseSync;
29
+ const requireFn = typeof require === 'function'
30
+ ? require
31
+ : createRequire(path.join(process.cwd(), '__adhdev_sqlite_loader__.js'));
32
+ const sqliteModule = requireFn(`node:${'sqlite'}`) as {
33
+ DatabaseSync: typeof CachedDatabaseSync;
34
+ };
35
+ CachedDatabaseSync = sqliteModule.DatabaseSync;
36
+ if (!CachedDatabaseSync) {
37
+ throw new Error('node:sqlite DatabaseSync unavailable');
38
+ }
39
+ return CachedDatabaseSync;
40
+ }
41
+
19
42
  export class CliProviderInstance implements ProviderInstance {
20
43
  readonly type: string;
21
44
  readonly category = 'cli' as const;
@@ -34,6 +57,17 @@ export class CliProviderInstance implements ProviderInstance {
34
57
  readonly instanceId: string;
35
58
 
36
59
  private presentationMode: 'terminal' | 'chat';
60
+ private providerSessionId?: string;
61
+ private launchMode: 'new' | 'resume' | 'manual';
62
+ private readonly startedAt = Date.now();
63
+ private onProviderSessionResolved?: (info: {
64
+ instanceId: string;
65
+ providerType: string;
66
+ providerName: string;
67
+ workspace: string;
68
+ providerSessionId: string;
69
+ previousProviderSessionId?: string;
70
+ }) => void;
37
71
 
38
72
  constructor(
39
73
  private provider: ProviderModule,
@@ -41,10 +75,25 @@ export class CliProviderInstance implements ProviderInstance {
41
75
  private cliArgs: string[] = [],
42
76
  instanceId?: string,
43
77
  transportFactory?: PtyTransportFactory,
78
+ options?: {
79
+ providerSessionId?: string;
80
+ launchMode?: 'new' | 'resume' | 'manual';
81
+ onProviderSessionResolved?: (info: {
82
+ instanceId: string;
83
+ providerType: string;
84
+ providerName: string;
85
+ workspace: string;
86
+ providerSessionId: string;
87
+ previousProviderSessionId?: string;
88
+ }) => void;
89
+ },
44
90
  ) {
45
91
  this.type = provider.type;
46
92
  this.instanceId = instanceId || crypto.randomUUID();
47
93
  this.presentationMode = 'chat';
94
+ this.providerSessionId = options?.providerSessionId;
95
+ this.launchMode = options?.launchMode || 'new';
96
+ this.onProviderSessionResolved = options?.onProviderSessionResolved;
48
97
  this.adapter = new ProviderCliAdapter(provider as any as CliProviderModule, workingDir, cliArgs, transportFactory);
49
98
  this.monitor = new StatusMonitor();
50
99
  this.historyWriter = new ChatHistoryWriter();
@@ -78,20 +127,71 @@ export class CliProviderInstance implements ProviderInstance {
78
127
 
79
128
  // PTY spawn
80
129
  await this.adapter.spawn();
130
+ if (this.providerSessionId && this.launchMode === 'resume') {
131
+ const resumedAt = Date.now();
132
+ this.historyWriter.appendSystemMarker(
133
+ this.type,
134
+ `Resumed saved session at ${this.formatMarkerTimestamp(resumedAt)}`,
135
+ {
136
+ instanceId: this.instanceId,
137
+ historySessionId: this.providerSessionId,
138
+ dedupKey: `resume:${this.providerSessionId}:${resumedAt}`,
139
+ receivedAt: resumedAt,
140
+ },
141
+ );
142
+ }
81
143
  }
82
144
 
83
145
  async onTick(): Promise<void> {
84
- // CLI is event-based so tick is unnecessary
85
- // Health check etc here if needed
146
+ if (this.providerSessionId) return;
147
+
148
+ let probedSessionId: string | null = null;
149
+ if (this.type === 'opencode-cli') {
150
+ probedSessionId = this.probeOpenCodeSessionId();
151
+ } else if (this.type === 'codex-cli') {
152
+ probedSessionId = this.probeCodexSessionId();
153
+ } else if (this.type === 'goose-cli') {
154
+ probedSessionId = this.probeGooseSessionId();
155
+ }
156
+
157
+ if (probedSessionId) {
158
+ this.promoteProviderSessionId(probedSessionId);
159
+ }
86
160
  }
87
161
 
88
162
  getState(): ProviderState {
89
163
  const adapterStatus = this.adapter.getStatus();
90
164
  const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
165
+ const parsedProviderSessionId = typeof parsedStatus?.providerSessionId === 'string'
166
+ ? parsedStatus.providerSessionId.trim()
167
+ : '';
168
+ if (parsedProviderSessionId) {
169
+ this.promoteProviderSessionId(parsedProviderSessionId);
170
+ }
91
171
  const runtime = this.adapter.getRuntimeMetadata();
172
+ const parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
92
173
 
93
174
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
94
175
 
176
+ if (parsedMessages.length > 0) {
177
+ let messagesToSave = parsedMessages;
178
+ if ((parsedStatus?.status === 'generating' || parsedStatus?.status === 'long_generating')) {
179
+ const lastIdx = messagesToSave.length - 1;
180
+ if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === 'assistant') {
181
+ messagesToSave = messagesToSave.slice(0, lastIdx);
182
+ }
183
+ }
184
+ if (messagesToSave.length > 0) {
185
+ this.historyWriter.appendNewMessages(
186
+ this.type,
187
+ messagesToSave,
188
+ parsedStatus?.title || dirName,
189
+ this.instanceId,
190
+ this.providerSessionId,
191
+ );
192
+ }
193
+ }
194
+
95
195
  return {
96
196
  type: this.type,
97
197
  name: this.provider.name,
@@ -100,14 +200,15 @@ export class CliProviderInstance implements ProviderInstance {
100
200
  mode: this.presentationMode,
101
201
  activeChat: {
102
202
  id: `${this.type}_${this.workingDir}`,
103
- title: parsedStatus?.title || `${this.provider.name} · ${dirName}`,
203
+ title: parsedStatus?.title || dirName,
104
204
  status: parsedStatus?.status || adapterStatus.status,
105
- messages: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [],
205
+ messages: parsedMessages,
106
206
  activeModal: parsedStatus?.activeModal ?? adapterStatus.activeModal,
107
207
  inputContent: '',
108
208
  },
109
209
  workspace: this.workingDir,
110
210
  instanceId: this.instanceId,
211
+ providerSessionId: this.providerSessionId,
111
212
  lastUpdated: Date.now(),
112
213
  settings: this.settings,
113
214
  pendingEvents: this.flushEvents(),
@@ -274,4 +375,104 @@ export class CliProviderInstance implements ProviderInstance {
274
375
 
275
376
  get cliType(): string { return this.type; }
276
377
  get cliName(): string { return this.provider.name; }
378
+
379
+ private formatMarkerTimestamp(timestamp: number): string {
380
+ const date = new Date(timestamp);
381
+ const pad = (value: number) => String(value).padStart(2, '0');
382
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
383
+ }
384
+
385
+ private promoteProviderSessionId(sessionId: string): void {
386
+ const nextSessionId = String(sessionId || '').trim();
387
+ if (!nextSessionId || nextSessionId === this.providerSessionId) return;
388
+
389
+ const previousHistorySessionId = this.providerSessionId || this.instanceId;
390
+ const previousProviderSessionId = this.providerSessionId;
391
+ this.providerSessionId = nextSessionId;
392
+ this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
393
+ this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
394
+ this.onProviderSessionResolved?.({
395
+ instanceId: this.instanceId,
396
+ providerType: this.type,
397
+ providerName: this.provider.name,
398
+ workspace: this.workingDir,
399
+ providerSessionId: nextSessionId,
400
+ previousProviderSessionId,
401
+ });
402
+ LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
403
+ }
404
+
405
+ private probeOpenCodeSessionId(): string | null {
406
+ const dbPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
407
+ if (!fs.existsSync(dbPath)) return null;
408
+ const minCreatedAt = Math.max(0, this.startedAt - 60_000);
409
+ const directories = this.getProbeDirectories();
410
+ const query = `select id from session where directory in (${this.buildSqlPlaceholderList(directories.length)}) and time_created >= ? and time_archived is null order by time_updated desc limit 1;`;
411
+ return this.querySqliteText(dbPath, query, [...directories, minCreatedAt]);
412
+ }
413
+
414
+ private probeCodexSessionId(): string | null {
415
+ const dbPath = path.join(os.homedir(), '.codex', 'state_5.sqlite');
416
+ if (!fs.existsSync(dbPath)) return null;
417
+ const minCreatedAt = Math.max(0, Math.floor((this.startedAt - 60_000) / 1000));
418
+ const directories = this.getProbeDirectories();
419
+ const query = `select id from threads where cwd in (${this.buildSqlPlaceholderList(directories.length)}) and created_at >= ? and archived = 0 order by created_at desc limit 1;`;
420
+ return this.querySqliteText(dbPath, query, [...directories, minCreatedAt]);
421
+ }
422
+
423
+ private probeGooseSessionId(): string | null {
424
+ const dbPath = path.join(os.homedir(), '.local', 'share', 'goose', 'sessions', 'sessions.db');
425
+ if (!fs.existsSync(dbPath)) return null;
426
+ const minCreatedAtIso = new Date(Math.max(0, this.startedAt - 60_000)).toISOString().slice(0, 19).replace('T', ' ');
427
+ const directories = this.getProbeDirectories();
428
+ const query = `select id from sessions where working_dir in (${this.buildSqlPlaceholderList(directories.length)}) and created_at >= ? order by updated_at desc limit 1;`;
429
+ try {
430
+ return this.querySqliteText(dbPath, query, [...directories, minCreatedAtIso]);
431
+ } catch {
432
+ return null;
433
+ }
434
+ }
435
+
436
+ private getProbeDirectories(): string[] {
437
+ const dirs = new Set<string>();
438
+ const addDir = (value: string | null | undefined) => {
439
+ const normalized = typeof value === 'string' ? value.trim() : '';
440
+ if (normalized) dirs.add(normalized);
441
+ };
442
+
443
+ addDir(this.workingDir);
444
+ try {
445
+ addDir(fs.realpathSync.native(this.workingDir));
446
+ } catch {
447
+ // noop
448
+ }
449
+
450
+ return Array.from(dirs);
451
+ }
452
+
453
+ private buildSqlPlaceholderList(count: number): string {
454
+ return Array.from({ length: count }, () => '?').join(', ');
455
+ }
456
+
457
+ private querySqliteText(dbPath: string, query: string, params: Array<string | number>): string | null {
458
+ let db: {
459
+ prepare(sql: string): { get(...values: Array<string | number>): unknown };
460
+ close(): void;
461
+ } | null = null;
462
+ try {
463
+ const DatabaseSync = getDatabaseSync();
464
+ db = new DatabaseSync(dbPath, { readOnly: true });
465
+ const row = db.prepare(query).get(...params) as { id?: unknown } | undefined;
466
+ const sessionId = typeof row?.id === 'string' ? row.id.trim() : '';
467
+ return sessionId || null;
468
+ } catch {
469
+ return null;
470
+ } finally {
471
+ try {
472
+ db?.close();
473
+ } catch {
474
+ // noop
475
+ }
476
+ }
477
+ }
277
478
  }
@@ -399,6 +399,9 @@ export interface ProviderResumeCapability {
399
399
  stopCommand?: string;
400
400
  shutdownGraceMs?: number;
401
401
  resumeArgs?: string[];
402
+ resumeSessionArgs?: string[];
403
+ newSessionArgs?: string[];
404
+ sessionIdFormat?: 'uuid' | 'string';
402
405
  }
403
406
 
404
407
  // ─── ACP Auth Types ─────────────────────────────────
@@ -61,6 +61,7 @@ interface ProviderStateBase {
61
61
  errorReason?: ProviderErrorReason;
62
62
  /** meta */
63
63
  instanceId: string;
64
+ providerSessionId?: string;
64
65
  lastUpdated: number;
65
66
  settings: Record<string, any>;
66
67
  /** Event queue (cleared after daemon collects) */
@@ -76,6 +76,7 @@ interface ProviderStateBase {
76
76
  errorReason?: ProviderErrorReason;
77
77
  /** meta */
78
78
  instanceId: string;
79
+ providerSessionId?: string;
79
80
  lastUpdated: number;
80
81
  settings: Record<string, any>;
81
82
  /** Event queue (cleared after daemon collects) */
@@ -64,6 +64,7 @@ export async function listHostedCliRuntimes(endpoint: SessionHostEndpoint): Prom
64
64
  cliType: record.providerType,
65
65
  workspace: record.workspace,
66
66
  cliArgs: Array.isArray(record.meta?.cliArgs) ? (record.meta.cliArgs as string[]) : [],
67
+ providerSessionId: typeof record.meta?.providerSessionId === 'string' ? String(record.meta.providerSessionId) : undefined,
67
68
  }));
68
69
  } finally {
69
70
  await client.close().catch(() => {});
@@ -41,6 +41,7 @@ export interface SessionEntry {
41
41
  parentId: string | null;
42
42
  providerType: string;
43
43
  providerName: string;
44
+ providerSessionId?: string;
44
45
  kind: SessionKind;
45
46
  transport: SessionTransport;
46
47
  status: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
@@ -152,6 +153,7 @@ export interface RecentLaunchEntry {
152
153
  providerType: string;
153
154
  providerName: string;
154
155
  kind: 'ide' | 'cli' | 'acp';
156
+ providerSessionId?: string;
155
157
  title?: string;
156
158
  workspace?: string | null;
157
159
  currentModel?: string;
@@ -94,6 +94,7 @@ export interface SessionEntry {
94
94
  parentId: string | null;
95
95
  providerType: string;
96
96
  providerName: string;
97
+ providerSessionId?: string;
97
98
  kind: SessionKind;
98
99
  transport: SessionTransport;
99
100
  status: SessionStatus;
@@ -221,6 +222,7 @@ export interface RecentLaunchEntry {
221
222
  providerType: string;
222
223
  providerName: string;
223
224
  kind: 'ide' | 'cli' | 'acp';
225
+ providerSessionId?: string;
224
226
  title?: string;
225
227
  workspace?: string | null;
226
228
  currentModel?: string;
@@ -259,6 +259,7 @@ function buildCliSession(state: CliProviderState): SessionEntry {
259
259
  parentId: null,
260
260
  providerType: state.type,
261
261
  providerName: state.name,
262
+ providerSessionId: state.providerSessionId,
262
263
  kind: 'agent',
263
264
  transport: 'pty',
264
265
  status: normalizeManagedStatus(activeChat?.status || state.status, {
@@ -183,6 +183,7 @@ function buildRecentLaunches(
183
183
  providerType: item.providerType,
184
184
  providerName: item.providerName,
185
185
  kind: item.kind,
186
+ providerSessionId: item.providerSessionId,
186
187
  title: item.title || item.providerName,
187
188
  workspace: item.workspace,
188
189
  currentModel: item.currentModel,