@adhdev/daemon-core 0.7.39 → 0.7.40

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.
@@ -17,7 +17,7 @@ export interface RuntimeAttachedClient {
17
17
  type: 'daemon' | 'web' | 'local-terminal';
18
18
  readOnly: boolean;
19
19
  }
20
- /** Session status union (used by SessionEntry.status, RecentSessionEntry.status, etc.) */
20
+ /** Session status union (used by SessionEntry.status, legacy recent-launch metadata, etc.) */
21
21
  export type SessionStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
22
22
  /** Inbox bucket categories for recent sessions */
23
23
  export type RecentSessionBucket = 'needs_attention' | 'working' | 'task_complete' | 'idle';
@@ -69,7 +69,6 @@ export interface SessionEntry {
69
69
  errorMessage?: string;
70
70
  errorReason?: _ProviderErrorReason;
71
71
  lastUpdated?: number;
72
- recentKey?: string;
73
72
  unread?: boolean;
74
73
  lastSeenAt?: number;
75
74
  inboxBucket?: RecentSessionBucket;
@@ -161,22 +160,15 @@ export interface DetectedIdeInfo {
161
160
  export type { RecentSessionBucket, TerminalBackendStatus } from './shared-types-extra.js';
162
161
  import type { RecentSessionBucket } from './shared-types-extra.js';
163
162
  import type { TerminalBackendStatus } from './shared-types-extra.js';
164
- export interface RecentSessionEntry {
163
+ export interface RecentLaunchEntry {
165
164
  id: string;
166
- recentKey: string;
167
- sessionId?: string | null;
168
165
  providerType: string;
169
166
  providerName: string;
170
167
  kind: 'ide' | 'cli' | 'acp';
171
- title: string;
168
+ title?: string;
172
169
  workspace?: string | null;
173
170
  currentModel?: string;
174
- status?: SessionEntry['status'];
175
- lastUsedAt: number;
176
- unread?: boolean;
177
- lastSeenAt?: number;
178
- inboxBucket?: RecentSessionBucket;
179
- surfaceHidden?: boolean;
171
+ lastLaunchedAt: number;
180
172
  }
181
173
  export interface StatusReportPayload {
182
174
  /** Unique daemon instance identifier */
@@ -206,7 +198,7 @@ export interface StatusReportPayload {
206
198
  workspaces?: WorkspaceEntry[];
207
199
  defaultWorkspaceId?: string | null;
208
200
  defaultWorkspacePath?: string | null;
209
- recentSessions?: RecentSessionEntry[];
201
+ recentLaunches?: RecentLaunchEntry[];
210
202
  terminalBackend?: TerminalBackendStatus;
211
203
  /** Available providers (present in StatusSnapshot, optional in raw payload) */
212
204
  availableProviders?: AvailableProviderInfo[];
@@ -35,4 +35,17 @@ export interface StatusSnapshotOptions {
35
35
  export interface StatusSnapshot extends StatusReportPayload {
36
36
  availableProviders: AvailableProviderInfo[];
37
37
  }
38
+ export declare function getSessionCompletionMarker(session: {
39
+ activeChat?: {
40
+ messages?: Array<{
41
+ role?: string;
42
+ id?: string;
43
+ index?: number;
44
+ timestamp?: number | string;
45
+ receivedAt?: number | string;
46
+ createdAt?: number | string;
47
+ _turnKey?: string;
48
+ }> | null;
49
+ } | null;
50
+ }): string;
38
51
  export declare function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnapshot;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.7.39",
3
+ "version": "0.7.40",
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.7.39",
3
+ "version": "0.7.40",
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",
@@ -18,12 +18,14 @@ import type { ProviderInstanceManager } from '../providers/provider-instance-man
18
18
  import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
19
19
  import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
20
20
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
21
- import { appendRecentActivity, buildRecentActivityKey, markRecentSessionSeen } from '../config/recent-activity.js';
21
+ import { appendRecentActivity, markSessionSeen } from '../config/recent-activity.js';
22
22
  import { detectIDEs } from '../detection/ide-detector.js';
23
23
  import { SessionRegistry } from '../sessions/registry.js';
24
24
  import { LOG } from '../logging/logger.js';
25
25
  import { logCommand } from '../logging/command-log.js';
26
26
  import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
27
+ import { buildSessionEntries } from '../status/builders.js';
28
+ import { getSessionCompletionMarker } from '../status/snapshot.js';
27
29
  import * as fs from 'fs';
28
30
 
29
31
  // ─── Types ───
@@ -61,6 +63,7 @@ const CHAT_COMMANDS = [
61
63
  'send_chat', 'new_chat', 'switch_chat', 'set_mode',
62
64
  'change_model',
63
65
  ];
66
+ const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
64
67
 
65
68
  export class DaemonCommandRouter {
66
69
  private deps: CommandRouterDeps;
@@ -265,28 +268,35 @@ export class DaemonCommandRouter {
265
268
  return { success: true, userName: name };
266
269
  }
267
270
 
268
- case 'mark_recent_seen': {
269
- const kind = args?.kind;
270
- const providerType = args?.providerType;
271
- if (!kind || !providerType) {
272
- return { success: false, error: 'kind and providerType are required' };
271
+ case 'mark_session_seen': {
272
+ const sessionId = args?.sessionId;
273
+ if (!sessionId || typeof sessionId !== 'string') {
274
+ return { success: false, error: 'sessionId is required' };
273
275
  }
274
- const recentKey = args?.recentKey || buildRecentActivityKey({
275
- kind,
276
- providerType,
277
- workspace: args?.workspace || null,
278
- });
279
- const next = markRecentSessionSeen(
280
- loadConfig(),
281
- recentKey,
276
+ const currentConfig = loadConfig();
277
+ const prevSeenAt = currentConfig.sessionReads?.[sessionId] || 0;
278
+ const sessionEntries = buildSessionEntries(
279
+ this.deps.instanceManager.collectAllStates(),
280
+ this.deps.cdpManagers as Map<string, any>,
281
+ );
282
+ const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
283
+ const completionMarker = targetSession ? getSessionCompletionMarker(targetSession) : '';
284
+ const next = markSessionSeen(
285
+ currentConfig,
286
+ sessionId,
282
287
  typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
288
+ completionMarker,
283
289
  );
290
+ if (READ_DEBUG_ENABLED) {
291
+ LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
292
+ }
284
293
  saveConfig(next);
285
294
  this.deps.onStatusChange?.();
286
295
  return {
287
296
  success: true,
288
- recentKey,
289
- seenAt: next.recentSessionReads?.[recentKey] || Date.now(),
297
+ sessionId,
298
+ seenAt: next.sessionReads?.[sessionId] || Date.now(),
299
+ completionMarker,
290
300
  };
291
301
  }
292
302
 
@@ -59,8 +59,10 @@ export interface ADHDevConfig {
59
59
 
60
60
  /** Unified recent activity across IDE / CLI / ACP launch flows */
61
61
  recentActivity?: RecentActivityEntry[];
62
- /** Last seen timestamps for machine-facing recent/session entries */
63
- recentSessionReads?: Record<string, number>;
62
+ /** Last seen timestamps for live sessions, keyed by sessionId */
63
+ sessionReads?: Record<string, number>;
64
+ /** Last seen completion marker for live sessions, keyed by sessionId */
65
+ sessionReadMarkers?: Record<string, string>;
64
66
 
65
67
  // Machine nickname (user-customizable label for this machine)
66
68
  machineNickname: string | null;
@@ -122,7 +124,8 @@ const DEFAULT_CONFIG: ADHDevConfig = {
122
124
  workspaces: [],
123
125
  defaultWorkspaceId: null,
124
126
  recentActivity: [],
125
- recentSessionReads: {},
127
+ sessionReads: {},
128
+ sessionReadMarkers: {},
126
129
  machineNickname: null,
127
130
  machineId: undefined,
128
131
  machineSecret: null,
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Unified recent activity — machine-facing "pick up where you left off".
2
+ * Unified recent activity — launcher-facing "pick up where you launched".
3
3
  *
4
- * Unlike cliHistory or workspaceActivity, this is task/session oriented:
4
+ * Unlike live session state, this is launch oriented:
5
5
  * - one normalized row shape for IDE / CLI / ACP
6
6
  * - deduped by kind + providerType + workspace
7
- * - optionally linked to a live sessionId when known
7
+ * - used only for quick-launch shortcuts
8
8
  */
9
9
 
10
10
  import * as path from 'path';
@@ -18,7 +18,6 @@ export interface RecentActivityEntry {
18
18
  providerName: string;
19
19
  workspace?: string | null;
20
20
  currentModel?: string;
21
- sessionId?: string | null;
22
21
  title?: string;
23
22
  lastUsedAt: number;
24
23
  }
@@ -62,22 +61,35 @@ export function getRecentActivity(config: ADHDevConfig, limit = 20): RecentActiv
62
61
  .slice(0, limit);
63
62
  }
64
63
 
65
- export function getRecentSessionSeenAt(config: ADHDevConfig, recentKey: string): number {
66
- return config.recentSessionReads?.[recentKey] || 0;
64
+ export function getSessionSeenAt(config: ADHDevConfig, sessionId: string): number {
65
+ return config.sessionReads?.[sessionId] || 0;
67
66
  }
68
67
 
69
- export function markRecentSessionSeen(
68
+ export function getSessionSeenMarker(config: ADHDevConfig, sessionId: string): string {
69
+ return config.sessionReadMarkers?.[sessionId] || '';
70
+ }
71
+
72
+ export function markSessionSeen(
70
73
  config: ADHDevConfig,
71
- recentKey: string,
74
+ sessionId: string,
72
75
  seenAt = Date.now(),
76
+ completionMarker?: string | null,
73
77
  ): ADHDevConfig {
74
- const prev = config.recentSessionReads || {};
75
- const nextSeenAt = Math.max(prev[recentKey] || 0, seenAt);
78
+ const prev = config.sessionReads || {};
79
+ const nextSeenAt = Math.max(prev[sessionId] || 0, seenAt);
80
+ const prevMarkers = config.sessionReadMarkers || {};
81
+ const nextMarker = typeof completionMarker === 'string' ? completionMarker : '';
76
82
  return {
77
83
  ...config,
78
- recentSessionReads: {
84
+ sessionReads: {
79
85
  ...prev,
80
- [recentKey]: nextSeenAt,
86
+ [sessionId]: nextSeenAt,
81
87
  },
88
+ sessionReadMarkers: nextMarker
89
+ ? {
90
+ ...prevMarkers,
91
+ [sessionId]: nextMarker,
92
+ }
93
+ : prevMarkers,
82
94
  };
83
95
  }
package/src/index.ts CHANGED
@@ -46,11 +46,11 @@ export type {
46
46
  // rollup-dts cannot resolve re-exports from shared-types.ts for them.
47
47
  import type { RuntimeWriteOwner as _RuntimeWriteOwner } from './shared-types-extra.js';
48
48
  import type { RuntimeAttachedClient as _RuntimeAttachedClient } from './shared-types-extra.js';
49
- import type { RecentSessionEntry as _RecentSessionEntry } from './shared-types.js';
49
+ import type { RecentLaunchEntry as _RecentLaunchEntry } from './shared-types.js';
50
50
  import type { TerminalBackendStatus as _TerminalBackendStatus } from './shared-types-extra.js';
51
51
  export type RuntimeWriteOwner = _RuntimeWriteOwner;
52
52
  export type RuntimeAttachedClient = _RuntimeAttachedClient;
53
- export type RecentSessionEntry = _RecentSessionEntry;
53
+ export type RecentLaunchEntry = _RecentLaunchEntry;
54
54
  export type TerminalBackendStatus = _TerminalBackendStatus;
55
55
 
56
56
  // Type aliases — rollup-dts cannot bundle re-exported type aliases at all.
@@ -20,7 +20,7 @@ export interface RuntimeAttachedClient {
20
20
  readOnly: boolean;
21
21
  }
22
22
 
23
- /** Session status union (used by SessionEntry.status, RecentSessionEntry.status, etc.) */
23
+ /** Session status union (used by SessionEntry.status, legacy recent-launch metadata, etc.) */
24
24
  export type SessionStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
25
25
 
26
26
  /** Inbox bucket categories for recent sessions */
@@ -147,17 +147,15 @@ export interface WorkspaceActivity {
147
147
  kind?: string;
148
148
  agentType?: string;
149
149
  }
150
- export interface RecentSessionEntry {
150
+ export interface RecentLaunchEntry {
151
151
  id: string;
152
- sessionId?: string | null;
153
152
  providerType: string;
154
153
  providerName: string;
155
154
  kind: 'ide' | 'cli' | 'acp';
156
- title: string;
155
+ title?: string;
157
156
  workspace?: string | null;
158
157
  currentModel?: string;
159
- status?: SessionEntry['status'];
160
- lastUsedAt: number;
158
+ lastLaunchedAt: number;
161
159
  }
162
160
  export interface StatusReportPayload {
163
161
  /** Daemon instance ID */
@@ -188,5 +186,5 @@ export interface StatusReportPayload {
188
186
  defaultWorkspaceId?: string | null;
189
187
  defaultWorkspacePath?: string | null;
190
188
  workspaceActivity?: WorkspaceActivity[];
191
- recentSessions?: RecentSessionEntry[];
189
+ recentLaunches?: RecentLaunchEntry[];
192
190
  }
@@ -120,7 +120,6 @@ export interface SessionEntry {
120
120
  errorMessage?: string;
121
121
  errorReason?: _ProviderErrorReason;
122
122
  lastUpdated?: number;
123
- recentKey?: string;
124
123
  unread?: boolean;
125
124
  lastSeenAt?: number;
126
125
  inboxBucket?: RecentSessionBucket;
@@ -215,22 +214,15 @@ export type { RecentSessionBucket, TerminalBackendStatus } from './shared-types-
215
214
  import type { RecentSessionBucket } from './shared-types-extra.js';
216
215
  import type { TerminalBackendStatus } from './shared-types-extra.js';
217
216
 
218
- export interface RecentSessionEntry {
217
+ export interface RecentLaunchEntry {
219
218
  id: string;
220
- recentKey: string;
221
- sessionId?: string | null;
222
219
  providerType: string;
223
220
  providerName: string;
224
221
  kind: 'ide' | 'cli' | 'acp';
225
- title: string;
222
+ title?: string;
226
223
  workspace?: string | null;
227
224
  currentModel?: string;
228
- status?: SessionEntry['status'];
229
- lastUsedAt: number;
230
- unread?: boolean;
231
- lastSeenAt?: number;
232
- inboxBucket?: RecentSessionBucket;
233
- surfaceHidden?: boolean;
225
+ lastLaunchedAt: number;
234
226
  }
235
227
 
236
228
  // ─── Status Report Payload (daemon → server) ────────────────────────
@@ -259,7 +251,7 @@ export interface StatusReportPayload {
259
251
  workspaces?: WorkspaceEntry[];
260
252
  defaultWorkspaceId?: string | null;
261
253
  defaultWorkspacePath?: string | null;
262
- recentSessions?: RecentSessionEntry[];
254
+ recentLaunches?: RecentLaunchEntry[];
263
255
  terminalBackend?: TerminalBackendStatus;
264
256
  /** Available providers (present in StatusSnapshot, optional in raw payload) */
265
257
  availableProviders?: AvailableProviderInfo[];
@@ -203,7 +203,7 @@ export class DaemonStatusReporter {
203
203
  currentModel: session.currentModel,
204
204
  currentPlan: session.currentPlan,
205
205
  currentAutoApprove: session.currentAutoApprove,
206
- recentKey: (session as any).recentKey,
206
+ lastUpdated: session.lastUpdated,
207
207
  unread: (session as any).unread,
208
208
  lastSeenAt: (session as any).lastSeenAt,
209
209
  inboxBucket: (session as any).inboxBucket,
@@ -8,17 +8,18 @@
8
8
 
9
9
  import * as os from 'os';
10
10
  import { loadConfig } from '../config/config.js';
11
- import { buildRecentActivityKey, getRecentActivity, getRecentSessionSeenAt } from '../config/recent-activity.js';
11
+ import { getRecentActivity, getSessionSeenAt, getSessionSeenMarker } from '../config/recent-activity.js';
12
12
  import { getWorkspaceState } from '../config/workspaces.js';
13
13
  import { getHostMemorySnapshot } from '../system/host-memory.js';
14
14
  import { getTerminalBackendRuntimeStatus } from '../cli-adapters/terminal-screen.js';
15
+ import { LOG } from '../logging/logger.js';
15
16
  import { buildSessionEntries, isCdpConnected } from './builders.js';
16
17
  import type { ProviderState } from '../providers/provider-instance.js';
17
18
  import type {
18
19
  AvailableProviderInfo,
19
20
  DetectedIdeInfo,
21
+ RecentLaunchEntry,
20
22
  RecentSessionBucket,
21
- RecentSessionEntry,
22
23
  SessionEntry,
23
24
  StatusReportPayload,
24
25
  } from '../shared-types.js';
@@ -53,6 +54,8 @@ export interface StatusSnapshot extends StatusReportPayload {
53
54
  availableProviders: AvailableProviderInfo[];
54
55
  }
55
56
 
57
+ const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
58
+
56
59
  function buildDetectedIdeInfos(
57
60
  detectedIdes: StatusSnapshotOptions['detectedIdes'],
58
61
  cdpManagers: StatusSnapshotOptions['cdpManagers'],
@@ -104,6 +107,30 @@ function getSessionMessageUpdatedAt(session: {
104
107
  );
105
108
  }
106
109
 
110
+ export function getSessionCompletionMarker(session: {
111
+ activeChat?: {
112
+ messages?: Array<{
113
+ role?: string;
114
+ id?: string;
115
+ index?: number;
116
+ timestamp?: number | string;
117
+ receivedAt?: number | string;
118
+ createdAt?: number | string;
119
+ _turnKey?: string;
120
+ }> | null
121
+ } | null
122
+ }) {
123
+ const lastMessage = session.activeChat?.messages?.at?.(-1) as any;
124
+ if (!lastMessage) return '';
125
+ const role = typeof lastMessage.role === 'string' ? lastMessage.role : '';
126
+ if (role === 'user' || role === 'human') return '';
127
+ if (typeof lastMessage._turnKey === 'string' && lastMessage._turnKey) return `turn:${lastMessage._turnKey}`;
128
+ if (typeof lastMessage.id === 'string' && lastMessage.id) return `id:${lastMessage.id}`;
129
+ if (typeof lastMessage.index === 'number' && Number.isFinite(lastMessage.index)) return `idx:${lastMessage.index}`;
130
+ const timestamp = parseMessageTime(lastMessage.timestamp) || parseMessageTime(lastMessage.receivedAt) || parseMessageTime(lastMessage.createdAt);
131
+ return timestamp > 0 ? `ts:${timestamp}` : '';
132
+ }
133
+
107
134
  function getSessionLastUsedAt(session: {
108
135
  activeChat?: {
109
136
  messages?: Array<{ timestamp?: number | string; receivedAt?: number | string; createdAt?: number | string }> | null
@@ -113,7 +140,7 @@ function getSessionLastUsedAt(session: {
113
140
  return getSessionMessageUpdatedAt(session) || session.lastUpdated || Date.now();
114
141
  }
115
142
 
116
- function getSessionKind(session: SessionEntry): RecentSessionEntry['kind'] {
143
+ function getSessionKind(session: SessionEntry): RecentLaunchEntry['kind'] {
117
144
  return session.transport === 'cdp-page' || session.transport === 'cdp-webview'
118
145
  ? 'ide'
119
146
  : session.transport === 'acp'
@@ -132,6 +159,8 @@ function getUnreadState(
132
159
  lastUsedAt: number,
133
160
  lastSeenAt: number,
134
161
  lastRole: string,
162
+ completionMarker: string,
163
+ seenCompletionMarker: string,
135
164
  ): { unread: boolean; inboxBucket: RecentSessionBucket } {
136
165
  if (status === 'waiting_approval') {
137
166
  return { unread: false, inboxBucket: 'needs_attention' };
@@ -139,88 +168,27 @@ function getUnreadState(
139
168
  if (status === 'generating' || status === 'starting') {
140
169
  return { unread: false, inboxBucket: 'working' };
141
170
  }
142
- const unread = hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human';
171
+ const unread = completionMarker
172
+ ? completionMarker !== seenCompletionMarker
173
+ : hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human';
143
174
  return { unread, inboxBucket: unread ? 'task_complete' : 'idle' };
144
175
  }
145
176
 
146
- function buildRecentSessions(
147
- sessions: ReturnType<typeof buildSessionEntries>,
177
+ function buildRecentLaunches(
148
178
  recentActivity: ReturnType<typeof getRecentActivity>,
149
- readState: Record<string, number>,
150
- ): RecentSessionEntry[] {
151
- const visibleKeys = new Set<string>();
152
- const hiddenKeys = new Set<string>();
153
- const live = sessions
154
- .filter((session) => !session.surfaceHidden && session.status !== 'stopped')
155
- .map((session) => {
156
- const kind = getSessionKind(session);
157
- const recentKey = buildRecentActivityKey({
158
- kind,
159
- providerType: session.providerType,
160
- workspace: session.workspace,
161
- });
162
- const lastSeenAt = readState[recentKey] || 0;
163
- const lastUsedAt = getSessionLastUsedAt(session);
164
- const { unread, inboxBucket } = getUnreadState(
165
- getSessionMessageUpdatedAt(session) > 0,
166
- session.status,
167
- lastUsedAt,
168
- lastSeenAt,
169
- getLastMessageRole(session),
170
- );
171
- return {
172
- id: session.id,
173
- recentKey,
174
- sessionId: session.id,
175
- providerType: session.providerType,
176
- providerName: session.providerName,
177
- kind,
178
- title: session.activeChat?.title || session.title || session.providerName,
179
- workspace: session.workspace,
180
- currentModel: session.currentModel,
181
- status: session.status,
182
- lastUsedAt,
183
- unread,
184
- lastSeenAt,
185
- inboxBucket,
186
- surfaceHidden: false,
187
- };
188
- });
189
- for (const item of live) {
190
- visibleKeys.add(`${item.kind}:${item.providerType}:${item.workspace || ''}`);
191
- }
192
- for (const session of sessions) {
193
- if (!session.surfaceHidden) continue;
194
- hiddenKeys.add(`${getSessionKind(session)}:${session.providerType}:${session.workspace || ''}`);
195
- }
196
- const persisted = recentActivity
197
- .filter((item) => {
198
- const key = `${item.kind}:${item.providerType}:${item.workspace || ''}`;
199
- return !visibleKeys.has(key) && !hiddenKeys.has(key);
200
- })
201
- .map((item) => {
202
- const lastSeenAt = readState[item.id] || 0;
203
- const unread = item.lastUsedAt > lastSeenAt;
204
- return {
205
- id: item.id,
206
- recentKey: item.id,
207
- sessionId: item.sessionId || null,
208
- providerType: item.providerType,
209
- providerName: item.providerName,
210
- kind: item.kind,
211
- title: item.title || item.providerName,
212
- workspace: item.workspace,
213
- currentModel: item.currentModel,
214
- lastUsedAt: item.lastUsedAt,
215
- unread,
216
- lastSeenAt,
217
- inboxBucket: unread ? 'task_complete' : 'idle' as RecentSessionBucket,
218
- surfaceHidden: false,
219
- };
220
- });
221
-
222
- return [...live, ...persisted]
223
- .sort((a, b) => b.lastUsedAt - a.lastUsedAt)
179
+ ): RecentLaunchEntry[] {
180
+ return recentActivity
181
+ .map((item) => ({
182
+ id: item.id,
183
+ providerType: item.providerType,
184
+ providerName: item.providerName,
185
+ kind: item.kind,
186
+ title: item.title || item.providerName,
187
+ workspace: item.workspace,
188
+ currentModel: item.currentModel,
189
+ lastLaunchedAt: item.lastUsedAt,
190
+ }))
191
+ .sort((a, b) => b.lastLaunchedAt - a.lastLaunchedAt)
224
192
  .slice(0, 12);
225
193
  }
226
194
 
@@ -233,16 +201,11 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
233
201
  options.allStates,
234
202
  options.cdpManagers as Map<string, any>,
235
203
  );
236
- const readState = cfg.recentSessionReads || {};
237
204
  for (const session of sessions) {
238
- const kind = getSessionKind(session);
239
- const recentKey = buildRecentActivityKey({
240
- kind,
241
- providerType: session.providerType,
242
- workspace: session.workspace,
243
- });
244
- const lastSeenAt = getRecentSessionSeenAt(cfg, recentKey);
205
+ const lastSeenAt = getSessionSeenAt(cfg, session.id);
206
+ const seenCompletionMarker = getSessionSeenMarker(cfg, session.id);
245
207
  const lastUsedAt = getSessionLastUsedAt(session);
208
+ const completionMarker = getSessionCompletionMarker(session);
246
209
  const { unread, inboxBucket } = session.surfaceHidden
247
210
  ? { unread: false, inboxBucket: 'idle' as RecentSessionBucket }
248
211
  : getUnreadState(
@@ -251,11 +214,18 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
251
214
  lastUsedAt,
252
215
  lastSeenAt,
253
216
  getLastMessageRole(session),
217
+ completionMarker,
218
+ seenCompletionMarker,
254
219
  );
255
- session.recentKey = recentKey;
256
220
  session.lastSeenAt = lastSeenAt;
257
221
  session.unread = unread;
258
222
  session.inboxBucket = inboxBucket;
223
+ if (READ_DEBUG_ENABLED && (session.unread || session.inboxBucket !== 'idle' || session.providerType.includes('codex'))) {
224
+ LOG.info(
225
+ 'RecentRead',
226
+ `snapshot session id=${session.id} provider=${session.providerType} status=${String(session.status || '')} bucket=${inboxBucket} unread=${String(unread)} lastSeenAt=${lastSeenAt} completionMarker=${completionMarker || '-'} seenMarker=${seenCompletionMarker || '-'} lastUpdated=${String(session.lastUpdated || 0)} lastUsedAt=${lastUsedAt} lastRole=${getLastMessageRole(session)} msgUpdatedAt=${getSessionMessageUpdatedAt(session)}`,
227
+ );
228
+ }
259
229
  }
260
230
  const terminalBackend = getTerminalBackendRuntimeStatus();
261
231
 
@@ -283,7 +253,7 @@ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnaps
283
253
  workspaces: wsState.workspaces,
284
254
  defaultWorkspaceId: wsState.defaultWorkspaceId,
285
255
  defaultWorkspacePath: wsState.defaultWorkspacePath,
286
- recentSessions: buildRecentSessions(sessions, recentActivity, readState),
256
+ recentLaunches: buildRecentLaunches(recentActivity),
287
257
  terminalBackend,
288
258
  availableProviders: buildAvailableProviders(options.providerLoader),
289
259
  };