@adhdev/daemon-core 0.9.22 → 0.9.24

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.
@@ -162,6 +162,7 @@ export interface SessionChatTailUpdate extends ReadChatSyncResult {
162
162
  interactionId?: string;
163
163
  seq: number;
164
164
  timestamp: number;
165
+ error?: string;
165
166
  }
166
167
  export interface SessionRuntimeOutputUpdate {
167
168
  topic: 'session.runtime_output';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.22",
3
+ "version": "0.9.24",
4
4
  "description": "ADHDev local session host core \u2014 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.9.22",
3
+ "version": "0.9.24",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -2414,8 +2414,12 @@ export class ProviderCliAdapter implements CliAdapter {
2414
2414
  if (buttonIndex in this.approvalKeys) {
2415
2415
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
2416
2416
  } else {
2417
+ const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
2418
+ const clampedIndex = buttonCount > 0
2419
+ ? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
2420
+ : Math.max(0, buttonIndex);
2417
2421
  const DOWN = '\x1B[B';
2418
- const keys = DOWN.repeat(Math.max(0, buttonIndex)) + '\r';
2422
+ const keys = DOWN.repeat(clampedIndex) + '\r';
2419
2423
  this.ptyProcess.write(keys);
2420
2424
  }
2421
2425
  }
@@ -423,8 +423,8 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
423
423
  } catch { /* noop */ }
424
424
  try {
425
425
  await this.client.close();
426
- } catch {
427
- if (destroy) throw new Error(`Failed to close session host client: ${this.options.runtimeId}`);
426
+ } catch (err) {
427
+ if (destroy) throw err instanceof Error ? err : new Error(`Failed to close session host client: ${this.options.runtimeId}`);
428
428
  }
429
429
  }
430
430
  }
@@ -193,14 +193,52 @@ function shouldCollapseReadChatReplayDuplicate(message: ChatMessage | null | und
193
193
  return role === 'assistant' || role === 'system';
194
194
  }
195
195
 
196
+ function normalizeReadChatReplayText(message: ChatMessage | null | undefined): string {
197
+ return flattenContent(message?.content || '').replace(/\s+/g, ' ').trim();
198
+ }
199
+
200
+ function isStableReadChatAssistantAnswer(message: ChatMessage | null | undefined): boolean {
201
+ if (!message) return false;
202
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
203
+ const kind = typeof message.kind === 'string' ? message.kind.trim().toLowerCase() : 'standard';
204
+ if (role !== 'assistant') return false;
205
+ if (kind && kind !== 'standard') return false;
206
+ const content = normalizeReadChatReplayText(message);
207
+ if (content.length < 160) return false;
208
+
209
+ // A provider may surface expanded command output as a standard assistant bubble
210
+ // (for example Claude Code's "Bash command ..." block). That is live work output,
211
+ // not a stable final answer. Treating it as a terminal answer would hide the
212
+ // real final response and violate read_chat fidelity.
213
+ if (/^(bash|shell|terminal) command\b/i.test(content)) return false;
214
+ return true;
215
+ }
216
+
217
+ function isReplayedAssistantAnswerAfterStableAnswer(
218
+ message: ChatMessage | null | undefined,
219
+ stableAnswer: ChatMessage | null,
220
+ ): boolean {
221
+ if (!message || !stableAnswer) return false;
222
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
223
+ const kind = typeof message.kind === 'string' ? message.kind.trim().toLowerCase() : 'standard';
224
+ if (role !== 'assistant') return false;
225
+ if (kind && kind !== 'standard') return false;
226
+ const content = normalizeReadChatReplayText(message);
227
+ const stableContent = normalizeReadChatReplayText(stableAnswer);
228
+ if (content.length < 80 || stableContent.length < 80) return false;
229
+ return content === stableContent || content.startsWith(stableContent) || stableContent.startsWith(content);
230
+ }
231
+
196
232
  function collapseReplayDuplicatesFromReadChat(messages: ChatMessage[]): ChatMessage[] {
197
233
  const collapsed: ChatMessage[] = [];
198
234
  const replaySignaturesInCurrentTurn = new Set<string>();
235
+ let stableAssistantAnswerInCurrentTurn: ChatMessage | null = null;
199
236
 
200
237
  for (const message of messages) {
201
238
  const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
202
239
  if (role === 'user') {
203
240
  replaySignaturesInCurrentTurn.clear();
241
+ stableAssistantAnswerInCurrentTurn = null;
204
242
  }
205
243
 
206
244
  const signature = buildReadChatReplayCollapseSignature(message);
@@ -210,12 +248,16 @@ function collapseReplayDuplicatesFromReadChat(messages: ChatMessage[]): ChatMess
210
248
  if (shouldCollapseReadChatReplayDuplicate(message) && signature) {
211
249
  if (previousSignature === signature) continue;
212
250
  if (replaySignaturesInCurrentTurn.has(signature)) continue;
251
+ if (isReplayedAssistantAnswerAfterStableAnswer(message, stableAssistantAnswerInCurrentTurn)) continue;
213
252
  }
214
253
 
215
254
  collapsed.push(message);
216
255
  if (shouldCollapseReadChatReplayDuplicate(message) && signature) {
217
256
  replaySignaturesInCurrentTurn.add(signature);
218
257
  }
258
+ if (isStableReadChatAssistantAnswer(message)) {
259
+ stableAssistantAnswerInCurrentTurn = message;
260
+ }
219
261
  }
220
262
 
221
263
  return collapsed;
@@ -367,6 +367,25 @@ export class CliProviderInstance implements ProviderInstance {
367
367
  ? parsedMessages.slice(-historyMessageCount)
368
368
  : [];
369
369
  }
370
+ // committedMessages (adapterStatus.messages) is the adapter's accumulated
371
+ // conversation history — use it as a floor to prevent history from disappearing.
372
+ //
373
+ // waiting_approval: always override — the approval dialog fills the terminal,
374
+ // pushing prior conversation out of view; parsedMessages will be partial or empty
375
+ // regardless of historyMessageCount.
376
+ //
377
+ // Other active states (generating, long_generating, error): apply floor only
378
+ // when the script has not explicitly windowed via historyMessageCount, so
379
+ // intentional windowing is preserved. Excludes idle — scripts may legitimately
380
+ // return messages:[] when the CLI exits or a new session begins.
381
+ const committedMessages = Array.isArray(adapterStatus.messages) ? adapterStatus.messages : [];
382
+ const isActiveNonIdle = adapterStatus.status !== 'idle';
383
+ const shouldApplyCommittedFloor = parsedMessages.length < committedMessages.length
384
+ && (adapterStatus.status === 'waiting_approval'
385
+ || (isActiveNonIdle && historyMessageCount === null));
386
+ if (shouldApplyCommittedFloor) {
387
+ parsedMessages = normalizeChatMessages(committedMessages as any);
388
+ }
370
389
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
371
390
  const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
372
391
 
@@ -215,6 +215,7 @@ export interface SessionChatTailUpdate extends ReadChatSyncResult {
215
215
  interactionId?: string;
216
216
  seq: number;
217
217
  timestamp: number;
218
+ error?: string;
218
219
  }
219
220
 
220
221
  export interface SessionRuntimeOutputUpdate {