@adhdev/daemon-core 0.8.18 → 0.8.20

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.18",
3
+ "version": "0.8.20",
4
4
  "description": "ADHDev local session host core — session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.18",
3
+ "version": "0.8.20",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,8 @@ import * as crypto from 'crypto';
11
11
  import chalk from 'chalk';
12
12
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
13
13
  import { detectCLI } from '../detection/cli-detector.js';
14
- import { loadConfig, saveConfig } from '../config/config.js';
14
+ import { loadConfig } from '../config/config.js';
15
+ import { loadState, saveState } from '../config/state-store.js';
15
16
  import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
16
17
  import { appendRecentActivity } from '../config/recent-activity.js';
17
18
  import { upsertSavedProviderSession } from '../config/saved-sessions.js';
@@ -260,9 +261,9 @@ export class DaemonCliManager {
260
261
  title?: string;
261
262
  }): void {
262
263
  try {
263
- let nextConfig = appendRecentActivity(loadConfig(), entry);
264
+ let nextState = appendRecentActivity(loadState(), entry);
264
265
  if (entry.providerSessionId && (entry.kind === 'cli' || entry.kind === 'acp')) {
265
- nextConfig = upsertSavedProviderSession(nextConfig, {
266
+ nextState = upsertSavedProviderSession(nextState, {
266
267
  kind: entry.kind,
267
268
  providerType: entry.providerType,
268
269
  providerName: entry.providerName,
@@ -272,7 +273,7 @@ export class DaemonCliManager {
272
273
  title: entry.title,
273
274
  });
274
275
  }
275
- saveConfig(nextConfig);
276
+ saveState(nextState);
276
277
  } catch (e) {
277
278
  console.error(colorize('red', ` ✗ Failed to save recent activity: ${e}`));
278
279
  }
@@ -18,6 +18,7 @@ import type { ProviderLoader } from '../providers/provider-loader.js';
18
18
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
19
19
  import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
20
20
  import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
21
+ import { loadState, saveState } from '../config/state-store.js';
21
22
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
22
23
  import { appendRecentActivity, getRecentActivity, markSessionSeen } from '../config/recent-activity.js';
23
24
  import { getSavedProviderSessions } from '../config/saved-sessions.js';
@@ -172,9 +173,9 @@ export class DaemonCommandRouter {
172
173
  const offset = Math.max(0, Number(args?.offset) || 0);
173
174
  const limit = Math.max(1, Math.min(100, Number(args?.limit) || 30));
174
175
  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)
176
+ const state = loadState();
177
+ const savedSessions = getSavedProviderSessions(state, { providerType, kind });
178
+ const recentSessions = getRecentActivity(state, 200)
178
179
  .filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
179
180
  const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
180
181
  const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
@@ -284,18 +285,18 @@ export class DaemonCommandRouter {
284
285
  this.deps.onIdeConnected?.();
285
286
  if (result.success && resolvedWorkspace) {
286
287
  try {
287
- const next = appendRecentActivity(loadConfig(), {
288
+ const next = appendRecentActivity(loadState(), {
288
289
  kind: 'ide',
289
290
  providerType: result.ideId || ideKey,
290
291
  providerName: result.ideId || ideKey,
291
292
  workspace: resolvedWorkspace,
292
293
  title: result.ideId || ideKey,
293
294
  });
294
- saveConfig(next);
295
+ saveState(next);
295
296
  } catch { /* ignore activity persist errors */ }
296
297
  } else if (result.success && (result.ideId || ideKey)) {
297
298
  try {
298
- saveConfig(appendRecentActivity(loadConfig(), {
299
+ saveState(appendRecentActivity(loadState(), {
299
300
  kind: 'ide',
300
301
  providerType: result.ideId || ideKey,
301
302
  providerName: result.ideId || ideKey,
@@ -326,8 +327,8 @@ export class DaemonCommandRouter {
326
327
  if (!sessionId || typeof sessionId !== 'string') {
327
328
  return { success: false, error: 'sessionId is required' };
328
329
  }
329
- const currentConfig = loadConfig();
330
- const prevSeenAt = currentConfig.sessionReads?.[sessionId] || 0;
330
+ const currentState = loadState();
331
+ const prevSeenAt = currentState.sessionReads?.[sessionId] || 0;
331
332
  const sessionEntries = buildSessionEntries(
332
333
  this.deps.instanceManager.collectAllStates(),
333
334
  this.deps.cdpManagers as Map<string, any>,
@@ -335,7 +336,7 @@ export class DaemonCommandRouter {
335
336
  const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
336
337
  const completionMarker = targetSession ? getSessionCompletionMarker(targetSession) : '';
337
338
  const next = markSessionSeen(
338
- currentConfig,
339
+ currentState,
339
340
  sessionId,
340
341
  typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
341
342
  completionMarker,
@@ -343,7 +344,7 @@ export class DaemonCommandRouter {
343
344
  if (READ_DEBUG_ENABLED) {
344
345
  LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
345
346
  }
346
- saveConfig(next);
347
+ saveState(next);
347
348
  this.deps.onStatusChange?.();
348
349
  return {
349
350
  success: true,
@@ -4,11 +4,10 @@
4
4
  * Manages launcher config, machine auth, and user preferences.
5
5
  */
6
6
  import type { WorkspaceEntry } from './workspaces.js';
7
- import type { RecentActivityEntry } from './recent-activity.js';
8
- import type { SavedProviderSessionEntry } from './saved-sessions.js';
9
7
  export type { WorkspaceEntry } from './workspaces.js';
10
8
  export type { RecentActivityEntry } from './recent-activity.js';
11
9
  export type { SavedProviderSessionEntry } from './saved-sessions.js';
10
+ export type { DaemonState } from './state-store.js';
12
11
  export interface ADHDevConfig {
13
12
  serverUrl: string;
14
13
  selectedIde: string | null;
@@ -23,14 +22,6 @@ export interface ADHDevConfig {
23
22
  workspaces?: WorkspaceEntry[];
24
23
  /** Default workspace id (from workspaces[]) — never used implicitly for launch */
25
24
  defaultWorkspaceId?: string | null;
26
- /** Unified recent activity across IDE / CLI / ACP launch flows */
27
- recentActivity?: RecentActivityEntry[];
28
- /** Persistent resume-capable provider sessions keyed by providerSessionId */
29
- savedProviderSessions?: SavedProviderSessionEntry[];
30
- /** Last seen timestamps for live sessions, keyed by sessionId */
31
- sessionReads?: Record<string, number>;
32
- /** Last seen completion marker for live sessions, keyed by sessionId */
33
- sessionReadMarkers?: Record<string, string>;
34
25
  machineNickname: string | null;
35
26
  /**
36
27
  * Stable local machine ID (prefix: `mach_`) — generated locally on first run.
@@ -9,11 +9,10 @@ import { join } from 'path';
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
10
10
  import { randomUUID } from 'crypto';
11
11
  import type { WorkspaceEntry } from './workspaces.js';
12
- import type { RecentActivityEntry } from './recent-activity.js';
13
- import type { SavedProviderSessionEntry } from './saved-sessions.js';
14
12
  export type { WorkspaceEntry } from './workspaces.js';
15
13
  export type { RecentActivityEntry } from './recent-activity.js';
16
14
  export type { SavedProviderSessionEntry } from './saved-sessions.js';
15
+ export type { DaemonState } from './state-store.js';
17
16
 
18
17
  export interface ADHDevConfig {
19
18
  // Server connection
@@ -44,15 +43,6 @@ export interface ADHDevConfig {
44
43
  /** Default workspace id (from workspaces[]) — never used implicitly for launch */
45
44
  defaultWorkspaceId?: string | null;
46
45
 
47
- /** Unified recent activity across IDE / CLI / ACP launch flows */
48
- recentActivity?: RecentActivityEntry[];
49
- /** Persistent resume-capable provider sessions keyed by providerSessionId */
50
- savedProviderSessions?: SavedProviderSessionEntry[];
51
- /** Last seen timestamps for live sessions, keyed by sessionId */
52
- sessionReads?: Record<string, number>;
53
- /** Last seen completion marker for live sessions, keyed by sessionId */
54
- sessionReadMarkers?: Record<string, string>;
55
-
56
46
  // Machine nickname (user-customizable label for this machine)
57
47
  machineNickname: string | null;
58
48
 
@@ -102,10 +92,6 @@ const DEFAULT_CONFIG: ADHDevConfig = {
102
92
  enabledIdes: [],
103
93
  workspaces: [],
104
94
  defaultWorkspaceId: null,
105
- recentActivity: [],
106
- savedProviderSessions: [],
107
- sessionReads: {},
108
- sessionReadMarkers: {},
109
95
  machineNickname: null,
110
96
  machineId: undefined,
111
97
  machineSecret: null,
@@ -140,16 +126,6 @@ function asBoolean(value: unknown, fallback: boolean): boolean {
140
126
 
141
127
  function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: string | null } {
142
128
  const parsed = isPlainObject(raw) ? raw : {};
143
- const legacySessionReads = isPlainObject(parsed.recentSessionReads) ? parsed.recentSessionReads : {};
144
- const sessionReads = isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {};
145
- const mergedSessionReads = Object.fromEntries(
146
- Object.entries({ ...legacySessionReads, ...sessionReads })
147
- .filter(([, value]) => typeof value === 'number' && Number.isFinite(value))
148
- );
149
- const sessionReadMarkers = Object.fromEntries(
150
- Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
151
- .filter(([, value]) => typeof value === 'string')
152
- );
153
129
 
154
130
  return {
155
131
  serverUrl: typeof parsed.serverUrl === 'string' && parsed.serverUrl.trim()
@@ -165,10 +141,6 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
165
141
  enabledIdes: asStringArray(parsed.enabledIdes),
166
142
  workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces as WorkspaceEntry[] : [],
167
143
  defaultWorkspaceId: asNullableString(parsed.defaultWorkspaceId) ?? asNullableString(parsed.activeWorkspaceId),
168
- recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
169
- savedProviderSessions: Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions as SavedProviderSessionEntry[] : [],
170
- sessionReads: mergedSessionReads,
171
- sessionReadMarkers,
172
144
  machineNickname: asNullableString(parsed.machineNickname),
173
145
  machineId: asOptionalString(parsed.machineId),
174
146
  machineSecret: parsed.machineSecret === null ? null : asOptionalString(parsed.machineSecret),
@@ -227,6 +199,48 @@ function getConfigPath(): string {
227
199
  return join(getConfigDir(), 'config.json');
228
200
  }
229
201
 
202
+ /**
203
+ * One-time migration: move runtime state fields from config.json to state.json.
204
+ * Called eagerly during loadConfig so state is extracted before the config
205
+ * normalizer strips the unknown fields.
206
+ */
207
+ function migrateStateToStateFile(raw: Record<string, any>): void {
208
+ const statePath = join(getConfigDir(), 'state.json');
209
+ if (existsSync(statePath)) return;
210
+
211
+ const recentActivity = Array.isArray(raw.recentActivity) ? raw.recentActivity : [];
212
+ const savedProviderSessions = Array.isArray(raw.savedProviderSessions) ? raw.savedProviderSessions : [];
213
+ const legacySessionReads = isPlainObject(raw.recentSessionReads) ? raw.recentSessionReads : {};
214
+ const sessionReads = isPlainObject(raw.sessionReads) ? raw.sessionReads : {};
215
+ const sessionReadMarkers = isPlainObject(raw.sessionReadMarkers) ? raw.sessionReadMarkers : {};
216
+
217
+ const hasData = recentActivity.length > 0
218
+ || savedProviderSessions.length > 0
219
+ || Object.keys(sessionReads).length > 0
220
+ || Object.keys(legacySessionReads as object).length > 0
221
+ || Object.keys(sessionReadMarkers as object).length > 0;
222
+
223
+ if (!hasData) return;
224
+
225
+ const mergedReads = Object.fromEntries(
226
+ Object.entries({ ...legacySessionReads, ...sessionReads })
227
+ .filter(([, v]) => typeof v === 'number' && Number.isFinite(v as number))
228
+ );
229
+ const cleanedMarkers = Object.fromEntries(
230
+ Object.entries(sessionReadMarkers as Record<string, unknown>)
231
+ .filter(([, v]) => typeof v === 'string')
232
+ );
233
+
234
+ const state = {
235
+ recentActivity,
236
+ savedProviderSessions,
237
+ sessionReads: mergedReads,
238
+ sessionReadMarkers: cleanedMarkers,
239
+ };
240
+
241
+ writeFileSync(statePath, JSON.stringify(state, null, 2), { encoding: 'utf-8', mode: 0o600 });
242
+ }
243
+
230
244
  /**
231
245
  * Load configuration from disk
232
246
  */
@@ -244,6 +258,10 @@ export function loadConfig(): ADHDevConfig {
244
258
  try {
245
259
  const raw = readFileSync(configPath, 'utf-8');
246
260
  const parsed = JSON.parse(raw);
261
+
262
+ // One-time migration: move runtime state to ~/.adhdev/state.json
263
+ migrateStateToStateFile(parsed);
264
+
247
265
  const normalizedInput = normalizeConfig(parsed);
248
266
  const ensured = ensureMachineId(normalizedInput);
249
267
  const normalized = ensured.config as ADHDevConfig & { activeWorkspaceId?: string | null };
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import * as path from 'path';
11
- import type { ADHDevConfig } from './config.js';
11
+ import type { DaemonState } from './state-store.js';
12
12
  import { expandPath } from './workspaces.js';
13
13
 
14
14
  export interface RecentActivityEntry {
@@ -49,9 +49,9 @@ export function buildRecentActivityKeyForEntry(
49
49
  }
50
50
 
51
51
  export function appendRecentActivity(
52
- config: ADHDevConfig,
52
+ state: DaemonState,
53
53
  entry: Omit<RecentActivityEntry, 'id' | 'lastUsedAt'> & { lastUsedAt?: number },
54
- ): ADHDevConfig {
54
+ ): DaemonState {
55
55
  const nextEntry: RecentActivityEntry = {
56
56
  ...entry,
57
57
  workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
@@ -59,39 +59,39 @@ export function appendRecentActivity(
59
59
  lastUsedAt: entry.lastUsedAt || Date.now(),
60
60
  };
61
61
 
62
- const filtered = (config.recentActivity || []).filter((item) => item.id !== nextEntry.id);
62
+ const filtered = (state.recentActivity || []).filter((item) => item.id !== nextEntry.id);
63
63
  return {
64
- ...config,
64
+ ...state,
65
65
  recentActivity: [nextEntry, ...filtered].slice(0, MAX_ACTIVITY),
66
66
  };
67
67
  }
68
68
 
69
- export function getRecentActivity(config: ADHDevConfig, limit = 20): RecentActivityEntry[] {
70
- return [...(config.recentActivity || [])]
69
+ export function getRecentActivity(state: DaemonState, limit = 20): RecentActivityEntry[] {
70
+ return [...(state.recentActivity || [])]
71
71
  .sort((a, b) => b.lastUsedAt - a.lastUsedAt)
72
72
  .slice(0, limit);
73
73
  }
74
74
 
75
- export function getSessionSeenAt(config: ADHDevConfig, sessionId: string): number {
76
- return config.sessionReads?.[sessionId] || 0;
75
+ export function getSessionSeenAt(state: DaemonState, sessionId: string): number {
76
+ return state.sessionReads?.[sessionId] || 0;
77
77
  }
78
78
 
79
- export function getSessionSeenMarker(config: ADHDevConfig, sessionId: string): string {
80
- return config.sessionReadMarkers?.[sessionId] || '';
79
+ export function getSessionSeenMarker(state: DaemonState, sessionId: string): string {
80
+ return state.sessionReadMarkers?.[sessionId] || '';
81
81
  }
82
82
 
83
83
  export function markSessionSeen(
84
- config: ADHDevConfig,
84
+ state: DaemonState,
85
85
  sessionId: string,
86
86
  seenAt = Date.now(),
87
87
  completionMarker?: string | null,
88
- ): ADHDevConfig {
89
- const prev = config.sessionReads || {};
88
+ ): DaemonState {
89
+ const prev = state.sessionReads || {};
90
90
  const nextSeenAt = Math.max(prev[sessionId] || 0, seenAt);
91
- const prevMarkers = config.sessionReadMarkers || {};
91
+ const prevMarkers = state.sessionReadMarkers || {};
92
92
  const nextMarker = typeof completionMarker === 'string' ? completionMarker : '';
93
93
  return {
94
- ...config,
94
+ ...state,
95
95
  sessionReads: {
96
96
  ...prev,
97
97
  [sessionId]: nextSeenAt,
@@ -1,5 +1,5 @@
1
1
  import * as path from 'path';
2
- import type { ADHDevConfig } from './config.js';
2
+ import type { DaemonState } from './state-store.js';
3
3
  import { expandPath } from './workspaces.js';
4
4
 
5
5
  export interface SavedProviderSessionEntry {
@@ -31,14 +31,14 @@ export function buildSavedProviderSessionKey(providerSessionId: string) {
31
31
  }
32
32
 
33
33
  export function upsertSavedProviderSession(
34
- config: ADHDevConfig,
34
+ state: DaemonState,
35
35
  entry: Omit<SavedProviderSessionEntry, 'id' | 'createdAt' | 'lastUsedAt'> & { createdAt?: number; lastUsedAt?: number },
36
- ): ADHDevConfig {
36
+ ): DaemonState {
37
37
  const providerSessionId = typeof entry.providerSessionId === 'string' ? entry.providerSessionId.trim() : '';
38
- if (!providerSessionId) return config;
38
+ if (!providerSessionId) return state;
39
39
 
40
40
  const id = buildSavedProviderSessionKey(providerSessionId);
41
- const existing = (config.savedProviderSessions || []).find(item => item.id === id);
41
+ const existing = (state.savedProviderSessions || []).find(item => item.id === id);
42
42
  const nextEntry: SavedProviderSessionEntry = {
43
43
  id,
44
44
  kind: entry.kind,
@@ -52,18 +52,18 @@ export function upsertSavedProviderSession(
52
52
  lastUsedAt: entry.lastUsedAt || Date.now(),
53
53
  };
54
54
 
55
- const filtered = (config.savedProviderSessions || []).filter(item => item.id !== id);
55
+ const filtered = (state.savedProviderSessions || []).filter(item => item.id !== id);
56
56
  return {
57
- ...config,
57
+ ...state,
58
58
  savedProviderSessions: [nextEntry, ...filtered].slice(0, MAX_SAVED_SESSIONS),
59
59
  };
60
60
  }
61
61
 
62
62
  export function getSavedProviderSessions(
63
- config: ADHDevConfig,
63
+ state: DaemonState,
64
64
  filters?: { providerType?: string; kind?: SavedProviderSessionEntry['kind'] },
65
65
  ): SavedProviderSessionEntry[] {
66
- return [...(config.savedProviderSessions || [])]
66
+ return [...(state.savedProviderSessions || [])]
67
67
  .filter(entry => {
68
68
  if (filters?.providerType && entry.providerType !== filters.providerType) return false;
69
69
  if (filters?.kind && entry.kind !== filters.kind) return false;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ADHDev State Store — Runtime state persistence
3
+ *
4
+ * Separates volatile runtime state (sessions, activity, read markers)
5
+ * from static configuration (config.json).
6
+ *
7
+ * State is stored in ~/.adhdev/state.json
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { getConfigDir } from './config.js';
13
+ import type { RecentActivityEntry } from './recent-activity.js';
14
+ import type { SavedProviderSessionEntry } from './saved-sessions.js';
15
+
16
+ export interface DaemonState {
17
+ /** Unified recent activity across IDE / CLI / ACP launch flows */
18
+ recentActivity: RecentActivityEntry[];
19
+ /** Persistent resume-capable provider sessions keyed by providerSessionId */
20
+ savedProviderSessions: SavedProviderSessionEntry[];
21
+ /** Last seen timestamps for live sessions, keyed by sessionId */
22
+ sessionReads: Record<string, number>;
23
+ /** Last seen completion marker for live sessions, keyed by sessionId */
24
+ sessionReadMarkers: Record<string, string>;
25
+ }
26
+
27
+ const DEFAULT_STATE: DaemonState = {
28
+ recentActivity: [],
29
+ savedProviderSessions: [],
30
+ sessionReads: {},
31
+ sessionReadMarkers: {},
32
+ };
33
+
34
+ function isPlainObject(value: unknown): value is Record<string, any> {
35
+ return !!value && typeof value === 'object' && !Array.isArray(value);
36
+ }
37
+
38
+ function getStatePath(): string {
39
+ return join(getConfigDir(), 'state.json');
40
+ }
41
+
42
+ function normalizeState(raw: unknown): DaemonState {
43
+ const parsed = isPlainObject(raw) ? raw : {};
44
+
45
+ const sessionReads = Object.fromEntries(
46
+ Object.entries(isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {})
47
+ .filter(([, value]) => typeof value === 'number' && Number.isFinite(value as number))
48
+ );
49
+ const sessionReadMarkers = Object.fromEntries(
50
+ Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
51
+ .filter(([, value]) => typeof value === 'string')
52
+ );
53
+
54
+ return {
55
+ recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
56
+ savedProviderSessions: Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions as SavedProviderSessionEntry[] : [],
57
+ sessionReads,
58
+ sessionReadMarkers,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Load runtime state from disk
64
+ */
65
+ export function loadState(): DaemonState {
66
+ const statePath = getStatePath();
67
+
68
+ if (!existsSync(statePath)) {
69
+ return { ...DEFAULT_STATE };
70
+ }
71
+
72
+ try {
73
+ const raw = readFileSync(statePath, 'utf-8');
74
+ return normalizeState(JSON.parse(raw));
75
+ } catch {
76
+ return { ...DEFAULT_STATE };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Save runtime state to disk
82
+ */
83
+ export function saveState(state: DaemonState): void {
84
+ const statePath = getStatePath();
85
+ const normalized = normalizeState(state);
86
+ writeFileSync(statePath, JSON.stringify(normalized, null, 2), { encoding: 'utf-8', mode: 0o600 });
87
+ }
88
+
89
+ /**
90
+ * Reset runtime state
91
+ */
92
+ export function resetState(): void {
93
+ saveState({ ...DEFAULT_STATE });
94
+ }
package/src/index.ts CHANGED
@@ -69,6 +69,10 @@ export type { RecentActivityEntry } from './config/recent-activity.js';
69
69
  export { getSavedProviderSessions, upsertSavedProviderSession } from './config/saved-sessions.js';
70
70
  export type { SavedProviderSessionEntry } from './config/saved-sessions.js';
71
71
 
72
+ // ── State Store ──
73
+ export { loadState, saveState, resetState } from './config/state-store.js';
74
+ export type { DaemonState } from './config/state-store.js';
75
+
72
76
  // ── Detection ──
73
77
  export { detectIDEs } from './detection/ide-detector.js';
74
78
  export type { IDEInfo } from './detection/ide-detector.js';
@@ -8,6 +8,7 @@
8
8
 
9
9
  import * as os from 'os';
10
10
  import { loadConfig } from '../config/config.js';
11
+ import { loadState } from '../config/state-store.js';
11
12
  import { getRecentActivity, getSessionSeenAt, getSessionSeenMarker } from '../config/recent-activity.js';
12
13
  import { getWorkspaceState } from '../config/workspaces.js';
13
14
  import { getHostMemorySnapshot } from '../system/host-memory.js';
@@ -195,16 +196,17 @@ function buildRecentLaunches(
195
196
 
196
197
  export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnapshot {
197
198
  const cfg = loadConfig();
199
+ const state = loadState();
198
200
  const wsState = getWorkspaceState(cfg);
199
201
  const memSnap = getHostMemorySnapshot();
200
- const recentActivity = getRecentActivity(cfg, 20);
202
+ const recentActivity = getRecentActivity(state, 20);
201
203
  const sessions = buildSessionEntries(
202
204
  options.allStates,
203
205
  options.cdpManagers as Map<string, any>,
204
206
  );
205
207
  for (const session of sessions) {
206
- const lastSeenAt = getSessionSeenAt(cfg, session.id);
207
- const seenCompletionMarker = getSessionSeenMarker(cfg, session.id);
208
+ const lastSeenAt = getSessionSeenAt(state, session.id);
209
+ const seenCompletionMarker = getSessionSeenMarker(state, session.id);
208
210
  const lastUsedAt = getSessionLastUsedAt(session);
209
211
  const completionMarker = getSessionCompletionMarker(session);
210
212
  const { unread, inboxBucket } = session.surfaceHidden