@adhdev/daemon-core 0.8.25 → 0.8.27

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.
@@ -41,6 +41,15 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
41
41
  return typeof (this.provider.scripts as any)?.[name] === 'function';
42
42
  }
43
43
 
44
+ private parseMaybeJson(raw: unknown): any {
45
+ if (typeof raw !== 'string') return raw;
46
+ try {
47
+ return JSON.parse(raw);
48
+ } catch {
49
+ return raw;
50
+ }
51
+ }
52
+
44
53
  private summarizeRaw(raw: unknown): string {
45
54
  try {
46
55
  if (typeof raw === 'string') return raw.replace(/\s+/g, ' ').trim().slice(0, 240);
@@ -111,12 +120,32 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
111
120
  }
112
121
 
113
122
  async sendMessage(evaluate: AgentEvaluateFn, text: string): Promise<void> {
114
- const script = this.callScript('sendMessage', text);
123
+ const params = { message: text };
124
+ const script = this.callScript('sendMessage', params) || this.callScript('sendMessage', text);
115
125
  if (!script) throw new Error(`[${this.agentName}] sendMessage script not available`);
116
126
  const result = await evaluate(script) as string;
117
127
  if (result && typeof result === 'string' && result.startsWith('error:')) {
118
128
  throw new Error(`[${this.agentName}] sendMessage failed: ${result}`);
119
129
  }
130
+
131
+ const parsed = this.parseMaybeJson(result);
132
+ if (parsed === true) return;
133
+ if (typeof parsed === 'string') {
134
+ const normalized = parsed.trim().toLowerCase();
135
+ if (normalized === 'ok' || normalized === 'sent' || normalized === 'success' || normalized === 'true') {
136
+ return;
137
+ }
138
+ }
139
+ if (parsed && typeof parsed === 'object') {
140
+ if (parsed.sent === true || parsed.success === true || parsed.ok === true || parsed.submitted === true || parsed.dispatched === true) {
141
+ return;
142
+ }
143
+ if (typeof parsed.error === 'string' && parsed.error.trim()) {
144
+ throw new Error(`[${this.agentName}] sendMessage failed: ${parsed.error}`);
145
+ }
146
+ }
147
+
148
+ throw new Error(`[${this.agentName}] sendMessage was not confirmed`);
120
149
  }
121
150
 
122
151
  async resolveAction(evaluate: AgentEvaluateFn, action: string, button?: string): Promise<boolean> {
@@ -12,6 +12,7 @@ import { DaemonCdpInitializer, type CdpInitializerConfig } from '../cdp/initiali
12
12
  import { setupIdeInstance, type CdpSetupContext } from '../cdp/setup.js';
13
13
  import { DaemonCommandHandler } from '../commands/handler.js';
14
14
  import { DaemonCommandRouter, type CommandRouterDeps } from '../commands/router.js';
15
+ import type { SessionHostControlPlane } from '../commands/router.js';
15
16
  import {
16
17
  DaemonCliManager,
17
18
  type CliTransportFactoryParams,
@@ -51,6 +52,7 @@ export interface DaemonInitConfig {
51
52
  /** Router transport-specific callbacks */
52
53
  onStatusChange?: () => void;
53
54
  onPostChatCommand?: () => void;
55
+ sessionHostControl?: SessionHostControlPlane | null;
54
56
  getCdpLogFn?: (ideType: string) => (msg: string) => void;
55
57
 
56
58
  /** Additional callback after CDP manager created (transport-specific extras) */
@@ -245,6 +247,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
245
247
  onIdeConnected: () => poller?.start(),
246
248
  onStatusChange: config.onStatusChange,
247
249
  onPostChatCommand: config.onPostChatCommand,
250
+ sessionHostControl: config.sessionHostControl,
248
251
  getCdpLogFn: config.getCdpLogFn || ((ideType: string) => LOG.forComponent(`CDP:${ideType}`).asLogFn()),
249
252
  });
250
253
 
@@ -42,6 +42,9 @@ export interface PtyRuntimeMetadata {
42
42
  workspaceLabel?: string;
43
43
  writeOwner?: PtyRuntimeWriteOwner | null;
44
44
  attachedClients?: PtyRuntimeClientInfo[];
45
+ restoredFromStorage?: boolean;
46
+ recoveryState?: string | null;
47
+ recoveryError?: string | null;
45
48
  }
46
49
 
47
50
  export interface PtyRuntimeTransport {
@@ -305,6 +305,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
305
305
  }
306
306
 
307
307
  private handleEvent(event: SessionHostEvent): void {
308
+ if (!('sessionId' in event)) return;
308
309
  if (event.sessionId !== this.options.runtimeId) return;
309
310
  if ((event.type === 'session_started' || event.type === 'session_resumed') && typeof event.pid === 'number') {
310
311
  this.currentPid = event.pid;
@@ -383,6 +384,13 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
383
384
  type: client.type,
384
385
  readOnly: client.readOnly,
385
386
  })),
387
+ restoredFromStorage: record.meta?.restoredFromStorage === true,
388
+ recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
389
+ ? String(record.meta.runtimeRecoveryState)
390
+ : null,
391
+ recoveryError: typeof record.meta?.runtimeRecoveryError === 'string'
392
+ ? String(record.meta.runtimeRecoveryError)
393
+ : null,
386
394
  };
387
395
  }
388
396
 
@@ -303,7 +303,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
303
303
  _log(`Extension: ${provider?.type || 'unknown_extension'}`);
304
304
  // Method 1: provider sendMessage script via evaluateInSession
305
305
  try {
306
- const evalResult = await h.evaluateProviderScript('sendMessage', { MESSAGE: text }, 30000);
306
+ const evalResult = await h.evaluateProviderScript('sendMessage', { message: text }, 30000);
307
307
  if (evalResult?.result) {
308
308
  const parsed = parseMaybeJson(evalResult.result);
309
309
  if (didProviderConfirmSend(parsed)) {
@@ -338,7 +338,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
338
338
  }
339
339
 
340
340
  _log(`Targeting IDE: ${getCurrentManagerKey(h)}`);
341
- const sendScript = h.getProviderScript('sendMessage', { MESSAGE: text });
341
+ const sendScript = h.getProviderScript('sendMessage', { message: text });
342
342
  if (sendScript) {
343
343
  try {
344
344
  const result = await targetCdp.evaluate(sendScript, 30000);
@@ -215,9 +215,31 @@ export class DaemonCommandHandler implements CommandHelpers {
215
215
  if (provider?.scripts) {
216
216
  const fn = (provider.scripts as any)[scriptName];
217
217
  if (typeof fn === 'function') {
218
- const firstVal = params ? Object.values(params)[0] : undefined;
219
- const script = firstVal ? fn(firstVal) : fn();
220
- if (script) return script;
218
+ if (params && Object.keys(params).length > 0) {
219
+ const firstVal = Object.values(params)[0];
220
+ if (scriptName === 'sendMessage' && typeof firstVal === 'string') {
221
+ const legacyScript = fn(firstVal);
222
+ if (legacyScript) return legacyScript;
223
+ }
224
+ const script = fn(params);
225
+ if (script) {
226
+ const likelyLegacyObjectLeak =
227
+ typeof script === 'string'
228
+ && script.includes('[object Object]')
229
+ && typeof firstVal === 'string';
230
+ if (!likelyLegacyObjectLeak) return script;
231
+ }
232
+
233
+ if (firstVal !== undefined) {
234
+ const legacyScript = fn(firstVal);
235
+ if (legacyScript) return legacyScript;
236
+ }
237
+
238
+ if (script) return script;
239
+ } else {
240
+ const script = fn();
241
+ if (script) return script;
242
+ }
221
243
  }
222
244
  }
223
245
  return null;
@@ -14,6 +14,7 @@ import { registerExtensionProviders } from '../cdp/setup.js';
14
14
  import { DaemonCommandHandler } from './handler.js';
15
15
  import { DaemonCliManager } from './cli-manager.js';
16
16
  import { supportsExplicitSessionResume } from './cli-manager.js';
17
+ import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
17
18
  import type { ProviderLoader } from '../providers/provider-loader.js';
18
19
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
19
20
  import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
@@ -35,6 +36,18 @@ import * as fs from 'fs';
35
36
 
36
37
  // ─── Types ───
37
38
 
39
+ export interface SessionHostControlPlane {
40
+ getDiagnostics(payload?: { includeSessions?: boolean; limit?: number }): Promise<any>;
41
+ listSessions(): Promise<any[]>;
42
+ stopSession(sessionId: string): Promise<any>;
43
+ resumeSession(sessionId: string): Promise<any>;
44
+ restartSession(sessionId: string): Promise<any>;
45
+ sendSignal(sessionId: string, signal: string): Promise<any>;
46
+ forceDetachClient(sessionId: string, clientId: string): Promise<any>;
47
+ acquireWrite(payload: { sessionId: string; clientId: string; ownerType: 'agent' | 'user'; force?: boolean }): Promise<any>;
48
+ releaseWrite(payload: { sessionId: string; clientId: string }): Promise<any>;
49
+ }
50
+
38
51
  export interface CommandRouterDeps {
39
52
  commandHandler: DaemonCommandHandler;
40
53
  cliManager: DaemonCliManager;
@@ -56,6 +69,8 @@ export interface CommandRouterDeps {
56
69
  getCdpLogFn?: (ideType: string) => (msg: string) => void;
57
70
  /** Package name for upgrade detection ('adhdev' or '@adhdev/daemon-standalone') */
58
71
  packageName?: string;
72
+ /** Session host control plane */
73
+ sessionHostControl?: SessionHostControlPlane | null;
59
74
  }
60
75
 
61
76
  export interface CommandRouterResult {
@@ -70,6 +85,30 @@ const CHAT_COMMANDS = [
70
85
  ];
71
86
  const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
72
87
 
88
+ function toHostedCliRuntimeDescriptor(record: any): HostedCliRuntimeDescriptor | null {
89
+ if (!record || typeof record !== 'object') return null;
90
+ const runtimeId = typeof record.sessionId === 'string' ? record.sessionId : '';
91
+ const cliType = typeof record.providerType === 'string' ? record.providerType : '';
92
+ const workspace = typeof record.workspace === 'string' ? record.workspace : '';
93
+ if (!runtimeId || !cliType || !workspace) return null;
94
+ return {
95
+ runtimeId,
96
+ runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
97
+ displayName: typeof record.displayName === 'string' ? record.displayName : undefined,
98
+ workspaceLabel: typeof record.workspaceLabel === 'string' ? record.workspaceLabel : undefined,
99
+ lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle as HostedCliRuntimeDescriptor['lifecycle'] : undefined,
100
+ recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
101
+ ? String(record.meta.runtimeRecoveryState)
102
+ : null,
103
+ cliType,
104
+ workspace,
105
+ cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs as string[] : [],
106
+ providerSessionId: typeof record.meta?.providerSessionId === 'string'
107
+ ? String(record.meta.providerSessionId)
108
+ : undefined,
109
+ };
110
+ }
111
+
73
112
  export class DaemonCommandRouter {
74
113
  private deps: CommandRouterDeps;
75
114
 
@@ -159,6 +198,99 @@ export class DaemonCommandRouter {
159
198
  }
160
199
  }
161
200
 
201
+ case 'session_host_get_diagnostics': {
202
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
203
+ const diagnostics = await this.deps.sessionHostControl.getDiagnostics({
204
+ includeSessions: args?.includeSessions !== false,
205
+ limit: Number(args?.limit) || undefined,
206
+ });
207
+ return { success: true, diagnostics };
208
+ }
209
+
210
+ case 'session_host_list_sessions': {
211
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
212
+ const sessions = await this.deps.sessionHostControl.listSessions();
213
+ return { success: true, sessions };
214
+ }
215
+
216
+ case 'session_host_stop_session': {
217
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
218
+ const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
219
+ if (!sessionId) return { success: false, error: 'sessionId required' };
220
+ const record = await this.deps.sessionHostControl.stopSession(sessionId);
221
+ return { success: true, record };
222
+ }
223
+
224
+ case 'session_host_resume_session': {
225
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
226
+ const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
227
+ if (!sessionId) return { success: false, error: 'sessionId required' };
228
+ const record = await this.deps.sessionHostControl.resumeSession(sessionId);
229
+ const hosted = toHostedCliRuntimeDescriptor(record);
230
+ if (hosted) {
231
+ await this.deps.cliManager.restoreHostedSessions([hosted]);
232
+ }
233
+ return { success: true, record };
234
+ }
235
+
236
+ case 'session_host_restart_session': {
237
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
238
+ const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
239
+ if (!sessionId) return { success: false, error: 'sessionId required' };
240
+ const record = await this.deps.sessionHostControl.restartSession(sessionId);
241
+ const hosted = toHostedCliRuntimeDescriptor(record);
242
+ if (hosted) {
243
+ await this.deps.cliManager.restoreHostedSessions([hosted]);
244
+ }
245
+ return { success: true, record };
246
+ }
247
+
248
+ case 'session_host_send_signal': {
249
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
250
+ const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
251
+ const signal = typeof args?.signal === 'string' ? args.signal : '';
252
+ if (!sessionId) return { success: false, error: 'sessionId required' };
253
+ if (!signal) return { success: false, error: 'signal required' };
254
+ const record = await this.deps.sessionHostControl.sendSignal(sessionId, signal);
255
+ return { success: true, record };
256
+ }
257
+
258
+ case 'session_host_force_detach_client': {
259
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
260
+ const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
261
+ const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
262
+ if (!sessionId) return { success: false, error: 'sessionId required' };
263
+ if (!clientId) return { success: false, error: 'clientId required' };
264
+ const record = await this.deps.sessionHostControl.forceDetachClient(sessionId, clientId);
265
+ return { success: true, record };
266
+ }
267
+
268
+ case 'session_host_acquire_write': {
269
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
270
+ const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
271
+ const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
272
+ const ownerType = args?.ownerType === 'agent' ? 'agent' : 'user';
273
+ if (!sessionId) return { success: false, error: 'sessionId required' };
274
+ if (!clientId) return { success: false, error: 'clientId required' };
275
+ const record = await this.deps.sessionHostControl.acquireWrite({
276
+ sessionId,
277
+ clientId,
278
+ ownerType,
279
+ force: args?.force !== false,
280
+ });
281
+ return { success: true, record };
282
+ }
283
+
284
+ case 'session_host_release_write': {
285
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
286
+ const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
287
+ const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
288
+ if (!sessionId) return { success: false, error: 'sessionId required' };
289
+ if (!clientId) return { success: false, error: 'clientId required' };
290
+ const record = await this.deps.sessionHostControl.releaseWrite({ sessionId, clientId });
291
+ return { success: true, record };
292
+ }
293
+
162
294
  case 'list_saved_sessions': {
163
295
  const providerType = typeof args?.providerType === 'string'
164
296
  ? args.providerType.trim()
@@ -14,7 +14,7 @@ import type { ProviderModule } from './contracts.js';
14
14
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
15
15
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
16
16
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
17
- import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
17
+ import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
18
18
  import { StatusMonitor } from './status-monitor.js';
19
19
  import { ChatHistoryWriter, readChatHistory } from '../config/chat-history.js';
20
20
  import { LOG } from '../logging/logger.js';
@@ -133,6 +133,7 @@ export class CliProviderInstance implements ProviderInstance {
133
133
 
134
134
  // PTY spawn
135
135
  await this.adapter.spawn();
136
+ this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
136
137
  if (this.providerSessionId) {
137
138
  const restoredHistory = readChatHistory(this.type, 0, 200, this.providerSessionId);
138
139
  if (restoredHistory.messages.length > 0) {
@@ -246,6 +247,7 @@ export class CliProviderInstance implements ProviderInstance {
246
247
  this.promoteProviderSessionId(parsedProviderSessionId);
247
248
  }
248
249
  const runtime = this.adapter.getRuntimeMetadata();
250
+ this.maybeAppendRuntimeRecoveryMessage(runtime);
249
251
  const parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
250
252
  const controlValues = extractProviderControlValues(this.provider.controls, parsedStatus);
251
253
  if (controlValues) {
@@ -603,6 +605,32 @@ export class CliProviderInstance implements ProviderInstance {
603
605
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
604
606
  }
605
607
 
608
+ private maybeAppendRuntimeRecoveryMessage(runtime: PtyRuntimeMetadata | null): void {
609
+ if (!runtime?.restoredFromStorage || !runtime.runtimeId) return;
610
+
611
+ const recoveryState = String(runtime.recoveryState || '').trim();
612
+ if (!recoveryState) return;
613
+
614
+ let content = '';
615
+ if (recoveryState === 'auto_resumed') {
616
+ content = 'Session host restored this CLI after restart and reattached it from a saved snapshot.';
617
+ } else if (recoveryState === 'resume_failed') {
618
+ const errorSuffix = runtime.recoveryError ? ` Resume failed: ${runtime.recoveryError}` : '';
619
+ content = `Session host found this CLI after restart, but automatic resume failed.${errorSuffix}`;
620
+ } else if (recoveryState === 'host_restart_interrupted') {
621
+ content = 'Session host found this CLI in interrupted state after restart and is attempting to resume it.';
622
+ } else if (recoveryState === 'orphan_snapshot') {
623
+ content = 'Session host restored the last snapshot for this CLI, but the original runtime was not resumed automatically.';
624
+ } else {
625
+ content = `Session host restored this CLI after restart (${recoveryState}).`;
626
+ }
627
+
628
+ this.appendRuntimeSystemMessage(
629
+ content,
630
+ `runtime_recovery:${runtime.runtimeId}:${recoveryState}`,
631
+ );
632
+ }
633
+
606
634
  private appendRuntimeSystemMessage(content: string, dedupKey: string, receivedAt = Date.now()): void {
607
635
  const normalizedContent = String(content || '').trim();
608
636
  if (!normalizedContent) return;
@@ -25,6 +25,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
25
25
  private currentStatus: string = 'idle';
26
26
  private agentStreams: any[] = [];
27
27
  private messages: any[] = [];
28
+ private prevMessageHashes = new Map<string, number>();
28
29
  private activeModal: any = null;
29
30
  private currentModel: string = '';
30
31
  private currentMode: string = '';
@@ -104,7 +105,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
104
105
  if (event === 'stream_update') {
105
106
  // Reflect data collected from agent-stream-manager
106
107
  if (data?.streams) this.agentStreams = data.streams;
107
- if (data?.messages) this.messages = data.messages;
108
+ if (data?.messages) this.messages = this.assignReceivedAt(data.messages);
108
109
  if (data?.activeModal !== undefined) this.activeModal = data.activeModal;
109
110
  if (data?.model) this.currentModel = data.model;
110
111
  if (data?.mode) this.currentMode = data.mode;
@@ -132,6 +133,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
132
133
  dispose(): void {
133
134
  this.agentStreams = [];
134
135
  this.messages = [];
136
+ this.prevMessageHashes.clear();
135
137
  this.monitor.reset();
136
138
  this.appliedEffectKeys.clear();
137
139
  this.runtimeMessages = [];
@@ -315,6 +317,26 @@ export class ExtensionProviderInstance implements ProviderInstance {
315
317
  );
316
318
  }
317
319
 
320
+ /**
321
+ * Assign stable receivedAt to extension messages.
322
+ * Same pattern as IdeProviderInstance.readChat() prevByHash —
323
+ * preserves first-seen timestamp across polling cycles.
324
+ */
325
+ private assignReceivedAt(messages: any[]): any[] {
326
+ const now = Date.now();
327
+ const nextHashes = new Map<string, number>();
328
+
329
+ for (const msg of messages) {
330
+ const hash = `${msg.role}:${(msg.content || '').slice(0, 100)}`;
331
+ const prevTime = this.prevMessageHashes.get(hash);
332
+ msg.receivedAt = prevTime || now;
333
+ nextHashes.set(hash, msg.receivedAt);
334
+ }
335
+
336
+ this.prevMessageHashes = nextHashes;
337
+ return messages;
338
+ }
339
+
318
340
  private mergeConversationMessages(messages: any[]): ChatMessage[] {
319
341
  if (this.runtimeMessages.length === 0) return messages;
320
342
  return [...messages, ...this.runtimeMessages.map((entry) => entry.message)]
@@ -382,6 +404,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
382
404
  }
383
405
  this.agentStreams = [];
384
406
  this.messages = [];
407
+ this.prevMessageHashes.clear();
385
408
  this.activeModal = null;
386
409
  this.currentModel = '';
387
410
  this.currentMode = '';
@@ -64,6 +64,23 @@ function trimMessageForStatus(message: unknown, stringLimit: number): unknown {
64
64
  return trimStructuredStrings(message, stringLimit);
65
65
  }
66
66
 
67
+ /**
68
+ * Collapse timestamp / createdAt into receivedAt so downstream consumers
69
+ * only ever need to read a single canonical time field.
70
+ */
71
+ function normalizeMessageTime(message: unknown): unknown {
72
+ if (!message || typeof message !== 'object') return message;
73
+ const msg = message as Record<string, unknown>;
74
+ if (msg.receivedAt == null) {
75
+ const fallback = msg.timestamp ?? msg.createdAt;
76
+ if (fallback != null) {
77
+ const ts = typeof fallback === 'string' ? Date.parse(fallback as string) : Number(fallback);
78
+ if (Number.isFinite(ts) && ts > 0) msg.receivedAt = ts;
79
+ }
80
+ }
81
+ return msg;
82
+ }
83
+
67
84
  function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[] {
68
85
  if (!Array.isArray(messages) || messages.length === 0) return [];
69
86
 
@@ -72,11 +89,11 @@ function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[
72
89
  let totalBytes = 0;
73
90
 
74
91
  for (let i = recent.length - 1; i >= 0; i -= 1) {
75
- let normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT);
92
+ let normalized = normalizeMessageTime(trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT));
76
93
  let size = estimateBytes(normalized);
77
94
 
78
95
  if (size > STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT) {
79
- normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT);
96
+ normalized = normalizeMessageTime(trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT));
80
97
  size = estimateBytes(normalized);
81
98
  }
82
99
 
@@ -95,17 +95,12 @@ function parseMessageTime(value: unknown): number {
95
95
 
96
96
  function getSessionMessageUpdatedAt(session: {
97
97
  activeChat?: {
98
- messages?: Array<{ timestamp?: number | string; receivedAt?: number | string; createdAt?: number | string }> | null
98
+ messages?: Array<{ receivedAt?: number | string }> | null
99
99
  } | null
100
100
  }) {
101
101
  const lastMessage = session.activeChat?.messages?.at?.(-1);
102
102
  if (!lastMessage) return 0;
103
- return (
104
- parseMessageTime(lastMessage.timestamp)
105
- || parseMessageTime(lastMessage.receivedAt)
106
- || parseMessageTime(lastMessage.createdAt)
107
- || 0
108
- );
103
+ return parseMessageTime(lastMessage.receivedAt) || 0;
109
104
  }
110
105
 
111
106
  export function getSessionCompletionMarker(session: {
@@ -114,9 +109,7 @@ export function getSessionCompletionMarker(session: {
114
109
  role?: string;
115
110
  id?: string;
116
111
  index?: number;
117
- timestamp?: number | string;
118
112
  receivedAt?: number | string;
119
- createdAt?: number | string;
120
113
  _turnKey?: string;
121
114
  }> | null
122
115
  } | null
@@ -124,17 +117,17 @@ export function getSessionCompletionMarker(session: {
124
117
  const lastMessage = session.activeChat?.messages?.at?.(-1) as any;
125
118
  if (!lastMessage) return '';
126
119
  const role = typeof lastMessage.role === 'string' ? lastMessage.role : '';
127
- if (role === 'user' || role === 'human') return '';
120
+ if (role === 'user' || role === 'human' || role === 'system') return '';
128
121
  if (typeof lastMessage._turnKey === 'string' && lastMessage._turnKey) return `turn:${lastMessage._turnKey}`;
129
122
  if (typeof lastMessage.id === 'string' && lastMessage.id) return `id:${lastMessage.id}`;
130
123
  if (typeof lastMessage.index === 'number' && Number.isFinite(lastMessage.index)) return `idx:${lastMessage.index}`;
131
- const timestamp = parseMessageTime(lastMessage.timestamp) || parseMessageTime(lastMessage.receivedAt) || parseMessageTime(lastMessage.createdAt);
124
+ const timestamp = parseMessageTime(lastMessage.receivedAt);
132
125
  return timestamp > 0 ? `ts:${timestamp}` : '';
133
126
  }
134
127
 
135
128
  function getSessionLastUsedAt(session: {
136
129
  activeChat?: {
137
- messages?: Array<{ timestamp?: number | string; receivedAt?: number | string; createdAt?: number | string }> | null
130
+ messages?: Array<{ receivedAt?: number | string }> | null
138
131
  } | null
139
132
  lastUpdated?: number
140
133
  }) {
@@ -171,7 +164,7 @@ function getUnreadState(
171
164
  }
172
165
  const unread = completionMarker
173
166
  ? completionMarker !== seenCompletionMarker
174
- : hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human';
167
+ : hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human' && lastRole !== 'system';
175
168
  return { unread, inboxBucket: unread ? 'task_complete' : 'idle' };
176
169
  }
177
170