@adhdev/daemon-core 0.9.82-rc.92 → 0.9.82-rc.94

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.92",
3
+ "version": "0.9.82-rc.94",
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 } : {}),
@@ -1671,7 +1692,8 @@ export class ProviderCliAdapter implements CliAdapter {
1671
1692
  const cached = this.parsedStatusCache;
1672
1693
  const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
1673
1694
  if (
1674
- cached
1695
+ !this.providerOwnsTranscript()
1696
+ && cached
1675
1697
  && cached.responseBuffer === this.responseBuffer
1676
1698
  && cached.currentTurnScope === this.currentTurnScope
1677
1699
  && cached.recentOutputBuffer === this.recentOutputBuffer
@@ -1997,6 +2019,104 @@ export class ProviderCliAdapter implements CliAdapter {
1997
2019
  }
1998
2020
 
1999
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> {
2000
2120
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
2001
2121
  const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
2002
2122
  const allowInterventionPrompt = allowInputDuringGeneration
@@ -2009,6 +2129,20 @@ export class ProviderCliAdapter implements CliAdapter {
2009
2129
  await new Promise(resolve => setTimeout(resolve, 50));
2010
2130
  }
2011
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
+ }
2012
2146
  if (!allowInterventionPrompt) {
2013
2147
  await this.waitForInteractivePrompt();
2014
2148
  }
@@ -2021,15 +2155,6 @@ export class ProviderCliAdapter implements CliAdapter {
2021
2155
  }
2022
2156
  }
2023
2157
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
2024
- const parsedStatusBeforeSend = !allowInputDuringGeneration
2025
- ? (() => {
2026
- try {
2027
- return this.getScriptParsedStatus?.() || null;
2028
- } catch {
2029
- return null;
2030
- }
2031
- })()
2032
- : null;
2033
2158
  const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
2034
2159
  ? String(parsedStatusBeforeSend.status)
2035
2160
  : '';
@@ -2047,6 +2172,10 @@ export class ProviderCliAdapter implements CliAdapter {
2047
2172
  && !this.hasActionableApproval()
2048
2173
  && !parsedHasActionableModal;
2049
2174
  if (!terminalLooksIdle) {
2175
+ if (allowQueue) {
2176
+ this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
2177
+ return;
2178
+ }
2050
2179
  throw new Error(`${this.cliName} is still processing the previous prompt`);
2051
2180
  }
2052
2181
  }
@@ -2055,6 +2184,10 @@ export class ProviderCliAdapter implements CliAdapter {
2055
2184
  !this.clearStaleIdleResponseGuard('send_message_guard')
2056
2185
  && !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
2057
2186
  ) {
2187
+ if (allowQueue) {
2188
+ this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
2189
+ return;
2190
+ }
2058
2191
  throw new Error(`${this.cliName} is still processing the previous prompt`);
2059
2192
  }
2060
2193
  }
@@ -2305,6 +2438,9 @@ export class ProviderCliAdapter implements CliAdapter {
2305
2438
  this.pendingTerminalQueryTail = '';
2306
2439
  this.ptyOutputChunks = [];
2307
2440
  this.finishRetryCount = 0;
2441
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2442
+ this.pendingOutboundQueue = [];
2443
+ this.pendingOutboundFlushInFlight = false;
2308
2444
  if (this.ptyProcess) {
2309
2445
  this.ptyProcess.write('\x03');
2310
2446
  setTimeout(() => {
@@ -2326,6 +2462,9 @@ export class ProviderCliAdapter implements CliAdapter {
2326
2462
  this.pendingTerminalQueryTail = '';
2327
2463
  this.ptyOutputChunks = [];
2328
2464
  this.finishRetryCount = 0;
2465
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2466
+ this.pendingOutboundQueue = [];
2467
+ this.pendingOutboundFlushInFlight = false;
2329
2468
  if (this.ptyProcess) {
2330
2469
  try {
2331
2470
  if (typeof this.ptyProcess.detach === 'function') {
@@ -2356,6 +2495,9 @@ export class ProviderCliAdapter implements CliAdapter {
2356
2495
  this.ptyOutputChunks = [];
2357
2496
  if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2358
2497
  this.finishRetryCount = 0;
2498
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2499
+ this.pendingOutboundQueue = [];
2500
+ this.pendingOutboundFlushInFlight = false;
2359
2501
  this.resetTerminalScreen();
2360
2502
  this.ptyProcess?.clearBuffer?.();
2361
2503
  this.onStatusChange?.();
@@ -2495,6 +2637,14 @@ export class ProviderCliAdapter implements CliAdapter {
2495
2637
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
2496
2638
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
2497
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,
2498
2648
  lastOutputAt: this.lastOutputAt,
2499
2649
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2500
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 {
@@ -1187,18 +1187,6 @@ export class DaemonCliManager {
1187
1187
  } else if (currentStatus === 'starting') {
1188
1188
  currentStatus = getEffectiveAgentSendStatus(adapter);
1189
1189
  }
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
1190
  const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
1203
1191
  const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
1204
1192
  if (provider?.category === 'acp') {
@@ -1209,7 +1197,11 @@ export class DaemonCliManager {
1209
1197
  const message = input.textFallback;
1210
1198
  if (!message) throw new Error('message required for send_chat');
1211
1199
  await adapter.sendMessage(message);
1212
- return { success: true, status: 'generating' };
1200
+ return {
1201
+ success: true,
1202
+ status: BUSY_AGENT_STATUSES.has(currentStatus) ? currentStatus : 'generating',
1203
+ ...(BUSY_AGENT_STATUSES.has(currentStatus) ? { queued: true, queuedReason: 'agent_runtime_busy' } : {}),
1204
+ };
1213
1205
  } else if (action === 'clear_history') {
1214
1206
  if (typeof adapter.clearHistory === 'function') adapter.clearHistory();
1215
1207
  return { success: true, cleared: true };