@adhdev/daemon-core 0.9.82-rc.93 → 0.9.82-rc.95

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.93",
3
+ "version": "0.9.82-rc.95",
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",
@@ -110,6 +110,14 @@ interface SendMessageCompletion {
110
110
  rejectOnce: (error: unknown) => void;
111
111
  }
112
112
 
113
+ interface PendingOutboundMessage {
114
+ id: string;
115
+ role: 'user';
116
+ content: string;
117
+ queuedAt: number;
118
+ source: 'sendMessage';
119
+ }
120
+
113
121
  export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
114
122
  if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
115
123
  if (maxChars <= 0) return '';
@@ -186,6 +194,9 @@ export class ProviderCliAdapter implements CliAdapter {
186
194
  private idleFinishCandidate: IdleFinishCandidate | null = null;
187
195
  private finishRetryTimer: NodeJS.Timeout | null = null;
188
196
  private finishRetryCount = 0;
197
+ private pendingOutboundQueue: PendingOutboundMessage[] = [];
198
+ private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
199
+ private pendingOutboundFlushInFlight = false;
189
200
 
190
201
  // Resize redraw suppression
191
202
  private resizeSuppressUntil: number = 0;
@@ -1415,6 +1426,7 @@ export class ProviderCliAdapter implements CliAdapter {
1415
1426
  this.activeModal = null;
1416
1427
  this.setStatus('idle', 'response_finished');
1417
1428
  this.onStatusChange?.();
1429
+ this.schedulePendingOutboundFlush();
1418
1430
  }
1419
1431
 
1420
1432
  private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[]): boolean {
@@ -1445,6 +1457,7 @@ export class ProviderCliAdapter implements CliAdapter {
1445
1457
  this.activeModal = null;
1446
1458
  this.setStatus('idle', 'script_idle_commit');
1447
1459
  this.onStatusChange?.();
1460
+ this.schedulePendingOutboundFlush();
1448
1461
  this.recordTrace('script_idle_commit', {
1449
1462
  messageCount: parsedMessages.length,
1450
1463
  lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
@@ -1654,6 +1667,14 @@ export class ProviderCliAdapter implements CliAdapter {
1654
1667
  messages: [],
1655
1668
  workingDir: this.workingDir,
1656
1669
  activeModal: effectiveModal,
1670
+ pendingOutboundCount: this.pendingOutboundQueue.length,
1671
+ pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
1672
+ id: message.id,
1673
+ role: message.role,
1674
+ content: message.content,
1675
+ queuedAt: message.queuedAt,
1676
+ source: message.source,
1677
+ })),
1657
1678
  errorMessage: this.parseErrorMessage || undefined,
1658
1679
  errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
1659
1680
  ...(bufferState ? { bufferState } : {}),
@@ -1998,6 +2019,104 @@ export class ProviderCliAdapter implements CliAdapter {
1998
2019
  }
1999
2020
 
2000
2021
  async sendMessage(text: string): Promise<void> {
2022
+ await this.sendMessageNow(text, true);
2023
+ }
2024
+
2025
+ private enqueuePendingOutboundMessage(text: string, reason: string): void {
2026
+ const content = String(text || '');
2027
+ const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
2028
+ if (duplicate) {
2029
+ this.recordTrace('send_message_queued_duplicate_suppressed', {
2030
+ reason,
2031
+ queueLength: this.pendingOutboundQueue.length,
2032
+ text: summarizeCliTraceText(content, 500),
2033
+ });
2034
+ return;
2035
+ }
2036
+ const queuedAt = Date.now();
2037
+ const message: PendingOutboundMessage = {
2038
+ id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
2039
+ role: 'user',
2040
+ content,
2041
+ queuedAt,
2042
+ source: 'sendMessage',
2043
+ };
2044
+ this.pendingOutboundQueue.push(message);
2045
+ this.recordTrace('send_message_queued', {
2046
+ reason,
2047
+ queueLength: this.pendingOutboundQueue.length,
2048
+ queuedAt,
2049
+ text: summarizeCliTraceText(content, 500),
2050
+ });
2051
+ LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
2052
+ this.onStatusChange?.();
2053
+ }
2054
+
2055
+ private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
2056
+ if (this.provider.allowInputDuringGeneration === true) return null;
2057
+ if (this.hasActionableApproval()) return null;
2058
+ const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
2059
+ ? String(parsedStatusBeforeSend.status)
2060
+ : '';
2061
+ if (parsedSessionStatus === 'idle' && this.parsedStatusHasFinalAssistantMessage(parsedStatusBeforeSend)) return null;
2062
+ if (this.currentStatus === 'generating') return 'current_status_generating';
2063
+ if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
2064
+ const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
2065
+ const parsedHasActionableModal = Boolean(
2066
+ parsedModal
2067
+ && Array.isArray(parsedModal.buttons)
2068
+ && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
2069
+ );
2070
+ const terminalLooksIdle = this.currentStatus === 'idle'
2071
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
2072
+ && !this.isWaitingForResponse
2073
+ && !this.currentTurnScope
2074
+ && !this.hasActionableApproval()
2075
+ && !parsedHasActionableModal;
2076
+ return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
2077
+ }
2078
+ if (this.isWaitingForResponse && this.currentTurnScope) return 'active_turn_in_progress';
2079
+ return null;
2080
+ }
2081
+
2082
+ private schedulePendingOutboundFlush(delayMs = 0): void {
2083
+ if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
2084
+ this.pendingOutboundFlushTimer = setTimeout(() => {
2085
+ this.pendingOutboundFlushTimer = null;
2086
+ void this.flushPendingOutboundQueue();
2087
+ }, Math.max(0, delayMs));
2088
+ }
2089
+
2090
+ private async flushPendingOutboundQueue(): Promise<void> {
2091
+ if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
2092
+ if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) return;
2093
+ this.pendingOutboundFlushInFlight = true;
2094
+ try {
2095
+ while (this.pendingOutboundQueue.length > 0) {
2096
+ if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) break;
2097
+ const next = this.pendingOutboundQueue[0];
2098
+ this.recordTrace('send_message_queue_flush', {
2099
+ id: next.id,
2100
+ queuedAt: next.queuedAt,
2101
+ queueLength: this.pendingOutboundQueue.length,
2102
+ text: summarizeCliTraceText(next.content, 500),
2103
+ });
2104
+ try {
2105
+ await this.sendMessageNow(next.content, false);
2106
+ this.pendingOutboundQueue.shift();
2107
+ this.onStatusChange?.();
2108
+ } catch (error: any) {
2109
+ LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
2110
+ this.schedulePendingOutboundFlush(1000);
2111
+ break;
2112
+ }
2113
+ }
2114
+ } finally {
2115
+ this.pendingOutboundFlushInFlight = false;
2116
+ }
2117
+ }
2118
+
2119
+ private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
2001
2120
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
2002
2121
  const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
2003
2122
  const allowInterventionPrompt = allowInputDuringGeneration
@@ -2010,6 +2129,20 @@ export class ProviderCliAdapter implements CliAdapter {
2010
2129
  await new Promise(resolve => setTimeout(resolve, 50));
2011
2130
  }
2012
2131
  }
2132
+ const parsedStatusBeforeSend = !allowInputDuringGeneration
2133
+ ? (() => {
2134
+ try {
2135
+ return this.getScriptParsedStatus?.() || null;
2136
+ } catch {
2137
+ return null;
2138
+ }
2139
+ })()
2140
+ : null;
2141
+ const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
2142
+ if (allowQueue && queueReason) {
2143
+ this.enqueuePendingOutboundMessage(text, queueReason);
2144
+ return;
2145
+ }
2013
2146
  if (!allowInterventionPrompt) {
2014
2147
  await this.waitForInteractivePrompt();
2015
2148
  }
@@ -2022,15 +2155,6 @@ export class ProviderCliAdapter implements CliAdapter {
2022
2155
  }
2023
2156
  }
2024
2157
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
2025
- const parsedStatusBeforeSend = !allowInputDuringGeneration
2026
- ? (() => {
2027
- try {
2028
- return this.getScriptParsedStatus?.() || null;
2029
- } catch {
2030
- return null;
2031
- }
2032
- })()
2033
- : null;
2034
2158
  const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
2035
2159
  ? String(parsedStatusBeforeSend.status)
2036
2160
  : '';
@@ -2048,6 +2172,10 @@ export class ProviderCliAdapter implements CliAdapter {
2048
2172
  && !this.hasActionableApproval()
2049
2173
  && !parsedHasActionableModal;
2050
2174
  if (!terminalLooksIdle) {
2175
+ if (allowQueue) {
2176
+ this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
2177
+ return;
2178
+ }
2051
2179
  throw new Error(`${this.cliName} is still processing the previous prompt`);
2052
2180
  }
2053
2181
  }
@@ -2056,6 +2184,10 @@ export class ProviderCliAdapter implements CliAdapter {
2056
2184
  !this.clearStaleIdleResponseGuard('send_message_guard')
2057
2185
  && !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
2058
2186
  ) {
2187
+ if (allowQueue) {
2188
+ this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
2189
+ return;
2190
+ }
2059
2191
  throw new Error(`${this.cliName} is still processing the previous prompt`);
2060
2192
  }
2061
2193
  }
@@ -2306,6 +2438,9 @@ export class ProviderCliAdapter implements CliAdapter {
2306
2438
  this.pendingTerminalQueryTail = '';
2307
2439
  this.ptyOutputChunks = [];
2308
2440
  this.finishRetryCount = 0;
2441
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2442
+ this.pendingOutboundQueue = [];
2443
+ this.pendingOutboundFlushInFlight = false;
2309
2444
  if (this.ptyProcess) {
2310
2445
  this.ptyProcess.write('\x03');
2311
2446
  setTimeout(() => {
@@ -2327,6 +2462,9 @@ export class ProviderCliAdapter implements CliAdapter {
2327
2462
  this.pendingTerminalQueryTail = '';
2328
2463
  this.ptyOutputChunks = [];
2329
2464
  this.finishRetryCount = 0;
2465
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2466
+ this.pendingOutboundQueue = [];
2467
+ this.pendingOutboundFlushInFlight = false;
2330
2468
  if (this.ptyProcess) {
2331
2469
  try {
2332
2470
  if (typeof this.ptyProcess.detach === 'function') {
@@ -2357,6 +2495,9 @@ export class ProviderCliAdapter implements CliAdapter {
2357
2495
  this.ptyOutputChunks = [];
2358
2496
  if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2359
2497
  this.finishRetryCount = 0;
2498
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2499
+ this.pendingOutboundQueue = [];
2500
+ this.pendingOutboundFlushInFlight = false;
2360
2501
  this.resetTerminalScreen();
2361
2502
  this.ptyProcess?.clearBuffer?.();
2362
2503
  this.onStatusChange?.();
@@ -2496,6 +2637,14 @@ export class ProviderCliAdapter implements CliAdapter {
2496
2637
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
2497
2638
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
2498
2639
  responseBuffer: this.responseBuffer.slice(-1000),
2640
+ pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
2641
+ id: message.id,
2642
+ role: message.role,
2643
+ content: message.content,
2644
+ queuedAt: message.queuedAt,
2645
+ source: message.source,
2646
+ })),
2647
+ pendingOutboundCount: this.pendingOutboundQueue.length,
2499
2648
  lastOutputAt: this.lastOutputAt,
2500
2649
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2501
2650
  lastScreenChangeAt: this.lastScreenChangeAt,
@@ -28,6 +28,14 @@ export interface CliSessionStatus {
28
28
  messages: CliChatMessage[];
29
29
  workingDir: string;
30
30
  activeModal: { message: string; buttons: string[] } | null;
31
+ pendingOutboundCount?: number;
32
+ pendingOutboundMessages?: Array<{
33
+ id: string;
34
+ role: 'user';
35
+ content: string;
36
+ queuedAt: number;
37
+ source: string;
38
+ }>;
31
39
  errorMessage?: string;
32
40
  errorReason?: string;
33
41
  bufferState?: {
@@ -25,7 +25,7 @@ const RECENT_SEND_WINDOW_MS = 1200;
25
25
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
26
26
  const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
27
27
  const CLI_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
28
- const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli']);
28
+ const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli']);
29
29
  const recentSendByTarget = new Map<string, number>();
30
30
 
31
31
  interface ApprovalSelectableInstance extends ProviderInstance {
@@ -1366,7 +1366,13 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1366
1366
  returnedStatus: String(returnedStatus || ''),
1367
1367
  selectedMessageSource: (messageSource as any).selected,
1368
1368
  messageSource,
1369
- shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType, provider) && (messageSource as any).selected !== 'native-history',
1369
+ shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType, provider)
1370
+ && isNativeSourceCanonicalHistory(provider?.canonicalHistory)
1371
+ && (messageSource as any).selected !== 'native-history'
1372
+ && typeof (messageSource as any).fallbackReason === 'string'
1373
+ && (messageSource as any).fallbackReason.startsWith('native_history_')
1374
+ && (messageSource as any).fallbackReason !== 'native_history_not_checked'
1375
+ && !(selectedTranscriptAuthority === 'provider' && selectedCoverage === 'full'),
1370
1376
  parsedMsgCount: parsedRecord.messages.length,
1371
1377
  returnedMsgCount: selectedMessages.length,
1372
1378
  },
@@ -363,10 +363,14 @@ function detectExplicitProviderSessionId(
363
363
 
364
364
  const subcommands = resume?.sessionIdFromSubcommand;
365
365
  if (Array.isArray(subcommands) && subcommands.length > 0) {
366
+ const hasResumeSubcommand = args.some((arg) => subcommands.includes(arg));
366
367
  const subcommandSessionId = readSubcommandSessionId(args, subcommands);
367
368
  if (subcommandSessionId) {
368
369
  return { providerSessionId: subcommandSessionId, launchMode: 'resume' };
369
370
  }
371
+ if (hasResumeSubcommand) {
372
+ return { launchMode: 'resume' };
373
+ }
370
374
  }
371
375
 
372
376
  return { launchMode: 'manual' };
@@ -400,6 +404,12 @@ export function resolveCliSessionBinding(
400
404
  launchMode: explicit.launchMode,
401
405
  };
402
406
  }
407
+ if (explicit.launchMode === 'resume') {
408
+ return {
409
+ cliArgs: baseArgs,
410
+ launchMode: 'resume',
411
+ };
412
+ }
403
413
  if (explicit.launchMode === 'manual' && hasArg(baseArgs || [], ['--session-id'])) {
404
414
  return {
405
415
  cliArgs: baseArgs,
@@ -1187,18 +1197,6 @@ export class DaemonCliManager {
1187
1197
  } else if (currentStatus === 'starting') {
1188
1198
  currentStatus = getEffectiveAgentSendStatus(adapter);
1189
1199
  }
1190
- if (BUSY_AGENT_STATUSES.has(currentStatus)) {
1191
- return {
1192
- success: false,
1193
- code: 'agent_runtime_busy',
1194
- reason: 'agent_runtime_busy',
1195
- retryable: true,
1196
- retryRecommended: true,
1197
- status: currentStatus,
1198
- targetSessionId: args?.targetSessionId,
1199
- error: `CLI agent '${agentType}' is currently ${currentStatus}; retry after the current turn finishes.`,
1200
- };
1201
- }
1202
1200
  const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
1203
1201
  const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
1204
1202
  if (provider?.category === 'acp') {
@@ -1209,7 +1207,11 @@ export class DaemonCliManager {
1209
1207
  const message = input.textFallback;
1210
1208
  if (!message) throw new Error('message required for send_chat');
1211
1209
  await adapter.sendMessage(message);
1212
- return { success: true, status: 'generating' };
1210
+ return {
1211
+ success: true,
1212
+ status: BUSY_AGENT_STATUSES.has(currentStatus) ? currentStatus : 'generating',
1213
+ ...(BUSY_AGENT_STATUSES.has(currentStatus) ? { queued: true, queuedReason: 'agent_runtime_busy' } : {}),
1214
+ };
1213
1215
  } else if (action === 'clear_history') {
1214
1216
  if (typeof adapter.clearHistory === 'function') adapter.clearHistory();
1215
1217
  return { success: true, cleared: true };