@adhdev/daemon-core 0.9.32 → 0.9.34

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.
@@ -5,7 +5,7 @@
5
5
  * collectCliData() + status transition logic from daemon-status.ts moved here.
6
6
  */
7
7
  import { type ProviderModule } from './contracts.js';
8
- import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
8
+ import type { ProviderInstance, ProviderState, InstanceContext, HotChatSessionState } from './provider-instance.js';
9
9
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
10
10
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
11
11
  type PersistableCliHistoryMessage = {
@@ -89,6 +89,7 @@ export declare class CliProviderInstance implements ProviderInstance {
89
89
  getState(): ProviderState;
90
90
  setPresentationMode(mode: 'terminal' | 'chat'): void;
91
91
  getPresentationMode(): 'terminal' | 'chat';
92
+ getHotChatSessionState(): HotChatSessionState;
92
93
  updateSettings(newSettings: Record<string, any>): void;
93
94
  onEvent(event: string, data?: any): void;
94
95
  dispose(): void;
@@ -7,7 +7,7 @@
7
7
  * 3. Collect overall state
8
8
  * 4. Event collection and propagation
9
9
  */
10
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
10
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, HotChatSessionState } from './provider-instance.js';
11
11
  export declare class ProviderInstanceManager {
12
12
  private instances;
13
13
  private tickTimer;
@@ -45,6 +45,7 @@ export declare class ProviderInstanceManager {
45
45
  * + Propagate pending events to event listeners
46
46
  */
47
47
  collectAllStates(): ProviderState[];
48
+ collectHotChatSessionStates(): HotChatSessionState[];
48
49
  /**
49
50
  * Per-category status collect
50
51
  */
@@ -112,6 +112,17 @@ export interface ProviderEvent {
112
112
  timestamp: number;
113
113
  [key: string]: any;
114
114
  }
115
+ export interface HotChatSessionState {
116
+ id: string;
117
+ status?: unknown;
118
+ unread?: unknown;
119
+ inboxBucket?: unknown;
120
+ lastMessageAt?: unknown;
121
+ runtimeLifecycle?: unknown;
122
+ runtimeSurfaceKind?: unknown;
123
+ runtimeRestoredFromStorage?: unknown;
124
+ runtimeRecoveryState?: unknown;
125
+ }
115
126
  export interface InstanceContext {
116
127
  /** CDP connection (IDE/Extension) */
117
128
  cdp?: {
@@ -142,6 +153,12 @@ export interface ProviderInstance {
142
153
  onTick(): Promise<void>;
143
154
  /** Return current status */
144
155
  getState(): ProviderState;
156
+ /**
157
+ * Return the cheap session metadata needed to decide whether chat-tail
158
+ * subscriptions should be flushed. Implementations must avoid rich transcript
159
+ * parsing here; callers use this on P2P hot flush paths.
160
+ */
161
+ getHotChatSessionState?(): HotChatSessionState | HotChatSessionState[] | null;
145
162
  /** Receive event (external → Instance) */
146
163
  onEvent(event: string, data?: any): void;
147
164
  /** Update settings at runtime (called when user changes settings from dashboard) */
@@ -221,6 +221,7 @@ export declare class ProviderLoader {
221
221
  getMachineProviderConfig(type: string): MachineProviderConfig;
222
222
  setMachineProviderConfig(type: string, patch: Partial<MachineProviderConfig>): boolean;
223
223
  setMachineProviderEnabled(type: string, enabled: boolean): boolean;
224
+ private getEffectiveProviderAvailability;
224
225
  getMachineProviderStatus(type: string): ProviderMachineStatus;
225
226
  getSpawnArgs(type: string, fallback?: string[]): string[];
226
227
  private parseArgsSetting;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.32",
3
+ "version": "0.9.34",
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.32",
3
+ "version": "0.9.34",
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",
@@ -1067,7 +1067,7 @@ export class ProviderCliAdapter implements CliAdapter {
1067
1067
  }
1068
1068
 
1069
1069
  if (this.currentTurnScope && !lastParsedAssistant) {
1070
- LOG.info(
1070
+ LOG.debug(
1071
1071
  'CLI',
1072
1072
  `[${this.cliType}] Settled without assistant: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
1073
1073
  );
@@ -2013,9 +2013,52 @@ export class ProviderCliAdapter implements CliAdapter {
2013
2013
 
2014
2014
  private armResponseTimeout(): void {
2015
2015
  if (this.responseTimeout) clearTimeout(this.responseTimeout);
2016
+ const timeoutMs = this.timeouts.maxResponse;
2017
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
2018
+ this.responseTimeout = null;
2019
+ return;
2020
+ }
2016
2021
  this.responseTimeout = setTimeout(() => {
2017
- if (this.isWaitingForResponse) this.finishResponse();
2018
- }, this.timeouts.maxResponse);
2022
+ this.responseTimeout = null;
2023
+ if (!this.isWaitingForResponse) return;
2024
+
2025
+ const detectedStatusBeforeEval = this.runDetectStatus(this.recentOutputBuffer);
2026
+ this.recordTrace('response_timeout_check', {
2027
+ timeoutMs,
2028
+ detectedStatus: detectedStatusBeforeEval,
2029
+ currentStatus: this.currentStatus,
2030
+ isWaitingForResponse: this.isWaitingForResponse,
2031
+ hasActionableApproval: this.hasActionableApproval(),
2032
+ ...buildCliTraceParseSnapshot({
2033
+ accumulatedBuffer: this.accumulatedBuffer,
2034
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
2035
+ responseBuffer: this.responseBuffer,
2036
+ partialResponse: this.responseBuffer,
2037
+ scope: this.currentTurnScope,
2038
+ }),
2039
+ });
2040
+
2041
+ // maxResponse is a watchdog/checkpoint, not a completion signal. The old
2042
+ // behavior called finishResponse() unconditionally at the default 300s,
2043
+ // which fabricated idle transitions and downstream generating_completed
2044
+ // notifications while long-running CLIs were still generating. Re-run the
2045
+ // normal settled parser instead and keep the turn open unless the provider
2046
+ // actually reports an idle, commit-ready state.
2047
+ this.settledBuffer = this.recentOutputBuffer;
2048
+ this.evaluateSettled();
2049
+
2050
+ if (this.isWaitingForResponse && !this.hasActionableApproval()) {
2051
+ const detectedStatusAfterEval = this.runDetectStatus(this.recentOutputBuffer);
2052
+ this.recordTrace('response_timeout_kept_open', {
2053
+ timeoutMs,
2054
+ detectedStatusBeforeEval,
2055
+ detectedStatusAfterEval,
2056
+ currentStatus: this.currentStatus,
2057
+ isWaitingForResponse: this.isWaitingForResponse,
2058
+ });
2059
+ this.armResponseTimeout();
2060
+ }
2061
+ }, timeoutMs);
2019
2062
  }
2020
2063
 
2021
2064
  private writeSubmitKeyForRetry(mode: string): void {
@@ -31,6 +31,7 @@ export function hydrateCliParsedMessages(
31
31
  ): any[] {
32
32
  const { committedMessages, scope, lastOutputAt } = options;
33
33
  const referenceMessages = [...committedMessages];
34
+ const referenceComparables = referenceMessages.map((message) => normalizeComparableMessageContent(message?.content || ''));
34
35
  const usedReferenceIndexes = new Set<number>();
35
36
  const now = options.now ?? Date.now();
36
37
 
@@ -43,7 +44,7 @@ export function hydrateCliParsedMessages(
43
44
  sameIndex
44
45
  && !usedReferenceIndexes.has(parsedIndex)
45
46
  && sameIndex.role === role
46
- && normalizeComparableMessageContent(sameIndex.content) === normalizedContent
47
+ && referenceComparables[parsedIndex] === normalizedContent
47
48
  && typeof sameIndex.timestamp === 'number'
48
49
  && Number.isFinite(sameIndex.timestamp)
49
50
  ) {
@@ -55,7 +56,7 @@ export function hydrateCliParsedMessages(
55
56
  if (usedReferenceIndexes.has(i)) continue;
56
57
  const candidate = referenceMessages[i];
57
58
  if (!candidate || candidate.role !== role) continue;
58
- const candidateContent = normalizeComparableMessageContent(candidate.content);
59
+ const candidateContent = referenceComparables[i];
59
60
  if (!candidateContent) continue;
60
61
  const exactMatch = candidateContent === normalizedContent;
61
62
  const fuzzyMatch = candidateContent.includes(normalizedContent) || normalizedContent.includes(candidateContent);
@@ -297,6 +297,36 @@ function toHistoryPersistedMessages(messages: ChatMessage[]): Array<{
297
297
  }));
298
298
  }
299
299
 
300
+ function findLastMessageIndexBySignature(messages: ChatMessage[], signature: string): number {
301
+ if (!signature) return -1;
302
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
303
+ if (getChatMessageSignature(messages[index]) === signature) {
304
+ return index;
305
+ }
306
+ }
307
+ return -1;
308
+ }
309
+
310
+ function buildBoundedTailSync(messages: ChatMessage[], cursor: Required<ReadChatCursor>): {
311
+ syncMode: ReadChatSyncMode;
312
+ replaceFrom: number;
313
+ messages: ChatMessage[];
314
+ totalMessages: number;
315
+ lastMessageSignature: string;
316
+ } {
317
+ const totalMessages = messages.length;
318
+ const tailMessages = cursor.tailLimit > 0 && totalMessages > cursor.tailLimit
319
+ ? messages.slice(-cursor.tailLimit)
320
+ : messages;
321
+ return {
322
+ syncMode: 'full',
323
+ replaceFrom: 0,
324
+ messages: tailMessages,
325
+ totalMessages,
326
+ lastMessageSignature: getChatMessageSignature(messages[totalMessages - 1]),
327
+ };
328
+ }
329
+
300
330
  function computeReadChatSync(messages: ChatMessage[], cursor: Required<ReadChatCursor>): {
301
331
  syncMode: ReadChatSyncMode;
302
332
  replaceFrom: number;
@@ -338,6 +368,16 @@ function computeReadChatSync(messages: ChatMessage[], cursor: Required<ReadChatC
338
368
  };
339
369
  }
340
370
 
371
+ if (cursor.tailLimit > 0 && knownSignature === lastMessageSignature) {
372
+ return {
373
+ syncMode: 'noop',
374
+ replaceFrom: totalMessages,
375
+ messages: [],
376
+ totalMessages,
377
+ lastMessageSignature,
378
+ };
379
+ }
380
+
341
381
  if (knownMessageCount < totalMessages) {
342
382
  const anchorSignature = getChatMessageSignature(messages[knownMessageCount - 1]);
343
383
  if (anchorSignature === knownSignature) {
@@ -349,6 +389,20 @@ function computeReadChatSync(messages: ChatMessage[], cursor: Required<ReadChatC
349
389
  lastMessageSignature,
350
390
  };
351
391
  }
392
+
393
+ if (cursor.tailLimit > 0) {
394
+ const signatureIndex = findLastMessageIndexBySignature(messages, knownSignature);
395
+ if (signatureIndex >= 0) {
396
+ return {
397
+ syncMode: 'append',
398
+ replaceFrom: knownMessageCount,
399
+ messages: messages.slice(signatureIndex + 1),
400
+ totalMessages,
401
+ lastMessageSignature,
402
+ };
403
+ }
404
+ return buildBoundedTailSync(messages, cursor);
405
+ }
352
406
  }
353
407
 
354
408
  const replaceFrom = Math.max(0, Math.min(knownMessageCount - 1, totalMessages));
@@ -161,6 +161,52 @@ function killPid(pid: number): boolean {
161
161
  }
162
162
  }
163
163
 
164
+ function getWindowsProcessCommandLine(pid: number): string | null {
165
+ const pidFilter = `ProcessId=${pid}`;
166
+ try {
167
+ const psOut = execFileSync('powershell.exe', [
168
+ '-NoProfile',
169
+ '-NonInteractive',
170
+ '-ExecutionPolicy', 'Bypass',
171
+ '-Command',
172
+ `(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
173
+ ], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
174
+ if (psOut) return psOut;
175
+ } catch {
176
+ // fall through to wmic fallback
177
+ }
178
+
179
+ try {
180
+ const wmicOut = execFileSync('wmic', [
181
+ 'process', 'where', pidFilter, 'get', 'CommandLine',
182
+ ], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
183
+ if (wmicOut) return wmicOut;
184
+ } catch {
185
+ // noop
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function getProcessCommandLine(pid: number): string | null {
191
+ if (!Number.isFinite(pid) || pid <= 0) return null;
192
+ if (process.platform === 'win32') return getWindowsProcessCommandLine(pid);
193
+ try {
194
+ const text = execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
195
+ encoding: 'utf8',
196
+ timeout: 3000,
197
+ stdio: ['ignore', 'pipe', 'ignore'],
198
+ }).trim();
199
+ return text || null;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function isManagedSessionHostPid(pid: number): boolean {
206
+ const commandLine = getProcessCommandLine(pid);
207
+ return !!commandLine && /session-host-daemon/i.test(commandLine);
208
+ }
209
+
164
210
  async function waitForPidExit(pid: number, timeoutMs: number): Promise<void> {
165
211
  const start = Date.now();
166
212
  while (Date.now() - start < timeoutMs) {
@@ -173,12 +219,12 @@ async function waitForPidExit(pid: number, timeoutMs: number): Promise<void> {
173
219
  }
174
220
  }
175
221
 
176
- function stopSessionHostProcesses(appName: string): void {
222
+ export function stopSessionHostProcesses(appName: string): void {
177
223
  const pidFile = path.join(os.homedir(), '.adhdev', `${appName}-session-host.pid`);
178
224
  try {
179
225
  if (fs.existsSync(pidFile)) {
180
226
  const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
181
- if (Number.isFinite(pid)) {
227
+ if (Number.isFinite(pid) && pid !== process.pid && isManagedSessionHostPid(pid)) {
182
228
  killPid(pid);
183
229
  }
184
230
  }
@@ -191,20 +237,6 @@ function stopSessionHostProcesses(appName: string): void {
191
237
  // noop
192
238
  }
193
239
  }
194
-
195
- if (process.platform !== 'win32') {
196
- try {
197
- const raw = execFileSync('pgrep', ['-f', 'session-host-daemon'], { encoding: 'utf8' }).trim();
198
- for (const line of raw.split('\n')) {
199
- const pid = Number.parseInt(line.trim(), 10);
200
- if (Number.isFinite(pid)) {
201
- killPid(pid);
202
- }
203
- }
204
- } catch {
205
- // noop
206
- }
207
- }
208
240
  }
209
241
 
210
242
  function removeDaemonPidFile(): void {
@@ -12,7 +12,7 @@ import * as fs from 'fs';
12
12
  import { createRequire } from 'node:module';
13
13
  import { normalizeInputEnvelope, type ProviderModule, flattenContent } from './contracts.js';
14
14
  import { assertTextOnlyInput } from './provider-input-support.js';
15
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason } from './provider-instance.js';
15
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState } from './provider-instance.js';
16
16
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
17
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
18
18
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
@@ -487,6 +487,21 @@ export class CliProviderInstance implements ProviderInstance {
487
487
  return this.presentationMode;
488
488
  }
489
489
 
490
+ getHotChatSessionState(): HotChatSessionState {
491
+ const adapterStatus = this.adapter.getStatus();
492
+ const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
493
+ const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
494
+ const runtime = this.adapter.getRuntimeMetadata();
495
+ return {
496
+ id: this.instanceId,
497
+ status: visibleStatus,
498
+ runtimeLifecycle: runtime?.lifecycle ?? null,
499
+ runtimeSurfaceKind: runtime?.surfaceKind,
500
+ runtimeRestoredFromStorage: runtime?.restoredFromStorage === true,
501
+ runtimeRecoveryState: runtime?.recoveryState ?? null,
502
+ };
503
+ }
504
+
490
505
  updateSettings(newSettings: Record<string, any>): void {
491
506
  this.settings = { ...newSettings };
492
507
  this.adapter.updateRuntimeSettings?.(this.settings);
@@ -650,6 +665,15 @@ export class CliProviderInstance implements ProviderInstance {
650
665
  this.completedDebouncePending = { chatTitle, duration, timestamp: now };
651
666
  this.completedDebounceTimer = setTimeout(() => {
652
667
  if (this.completedDebouncePending) {
668
+ const latestStatus = this.adapter.getStatus();
669
+ const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
670
+ const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
671
+ if (latestVisibleStatus !== 'idle') {
672
+ LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
673
+ this.completedDebouncePending = null;
674
+ this.completedDebounceTimer = null;
675
+ return;
676
+ }
653
677
  LOG.info('CLI', `[${this.type}] completed in ${this.completedDebouncePending.duration}s`);
654
678
  this.pushEvent({ event: 'agent:generating_completed', ...this.completedDebouncePending });
655
679
  this.completedDebouncePending = null;
@@ -8,9 +8,25 @@
8
8
  * 4. Event collection and propagation
9
9
  */
10
10
 
11
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
11
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, HotChatSessionState } from './provider-instance.js';
12
12
  import { LOG } from '../logging/logger.js';
13
13
 
14
+ function projectHotChatSessionStatesFromProviderState(state: ProviderState): HotChatSessionState[] {
15
+ const project = (item: ProviderState): HotChatSessionState => ({
16
+ id: item.instanceId,
17
+ status: item.activeChat?.status || item.status,
18
+ runtimeLifecycle: item.runtime?.lifecycle ?? null,
19
+ runtimeSurfaceKind: item.runtime?.surfaceKind,
20
+ runtimeRestoredFromStorage: item.runtime?.restoredFromStorage === true,
21
+ runtimeRecoveryState: item.runtime?.recoveryState ?? null,
22
+ });
23
+
24
+ if (state.category === 'ide') {
25
+ return [project(state), ...state.extensions.map(project)];
26
+ }
27
+ return [project(state)];
28
+ }
29
+
14
30
  export class ProviderInstanceManager {
15
31
  private instances = new Map<string, ProviderInstance>();
16
32
  private tickTimer: NodeJS.Timeout | null = null;
@@ -120,6 +136,32 @@ export class ProviderInstanceManager {
120
136
  return states;
121
137
  }
122
138
 
139
+ collectHotChatSessionStates(): HotChatSessionState[] {
140
+ const sessions: HotChatSessionState[] = [];
141
+ for (const [id, instance] of this.instances) {
142
+ try {
143
+ const projected = instance.getHotChatSessionState?.();
144
+ if (Array.isArray(projected)) {
145
+ sessions.push(...projected.filter((session): session is HotChatSessionState => !!session?.id));
146
+ continue;
147
+ }
148
+ if (projected?.id) {
149
+ sessions.push(projected);
150
+ continue;
151
+ }
152
+
153
+ // Fallback for provider types that have not implemented the cheap
154
+ // projection yet. CLI implements getHotChatSessionState() because
155
+ // its full getState() may run rich transcript parsing.
156
+ const state = instance.getState();
157
+ sessions.push(...projectHotChatSessionStatesFromProviderState(state));
158
+ } catch (e) {
159
+ LOG.warn('InstanceMgr', `[InstanceManager] Failed to collect hot chat metadata from ${id}: ${(e as Error).message}`);
160
+ }
161
+ }
162
+ return sessions;
163
+ }
164
+
123
165
  /**
124
166
  * Per-category status collect
125
167
  */
@@ -135,6 +135,18 @@ export interface ProviderEvent {
135
135
  [key: string]: any;
136
136
  }
137
137
 
138
+ export interface HotChatSessionState {
139
+ id: string;
140
+ status?: unknown;
141
+ unread?: unknown;
142
+ inboxBucket?: unknown;
143
+ lastMessageAt?: unknown;
144
+ runtimeLifecycle?: unknown;
145
+ runtimeSurfaceKind?: unknown;
146
+ runtimeRestoredFromStorage?: unknown;
147
+ runtimeRecoveryState?: unknown;
148
+ }
149
+
138
150
  // ─── ProviderInstance interface ─────────────────
139
151
 
140
152
  export interface InstanceContext {
@@ -172,6 +184,13 @@ export interface ProviderInstance {
172
184
  /** Return current status */
173
185
  getState(): ProviderState;
174
186
 
187
+ /**
188
+ * Return the cheap session metadata needed to decide whether chat-tail
189
+ * subscriptions should be flushed. Implementations must avoid rich transcript
190
+ * parsing here; callers use this on P2P hot flush paths.
191
+ */
192
+ getHotChatSessionState?(): HotChatSessionState | HotChatSessionState[] | null;
193
+
175
194
  /** Receive event (external → Instance) */
176
195
  onEvent(event: string, data?: any): void;
177
196
 
@@ -653,10 +653,26 @@ export class ProviderLoader {
653
653
  return this.setMachineProviderConfig(type, { enabled });
654
654
  }
655
655
 
656
+ private getEffectiveProviderAvailability(type: string): ProviderAvailabilityState | undefined {
657
+ const providerType = this.resolveAlias(type);
658
+ const availability = this.providerAvailability.get(providerType);
659
+ if (availability) return availability;
660
+
661
+ const machineConfig = this.getMachineProviderConfig(providerType);
662
+ const lastDetection = machineConfig.lastDetection;
663
+ if (!lastDetection) return undefined;
664
+ return {
665
+ installed: lastDetection.ok === true,
666
+ detectedPath: typeof lastDetection.path === 'string' && lastDetection.path.trim()
667
+ ? lastDetection.path.trim()
668
+ : null,
669
+ };
670
+ }
671
+
656
672
  getMachineProviderStatus(type: string): ProviderMachineStatus {
657
673
  const providerType = this.resolveAlias(type);
658
674
  if (!this.isMachineProviderEnabled(providerType)) return 'disabled';
659
- const availability = this.providerAvailability.get(providerType);
675
+ const availability = this.getEffectiveProviderAvailability(providerType);
660
676
  if (!availability) return 'enabled_unchecked';
661
677
  return availability.installed ? 'detected' : 'not_detected';
662
678
  }
@@ -792,7 +808,7 @@ export class ProviderLoader {
792
808
 
793
809
  getAvailableProviderInfos(): Array<ProviderModule & { installed?: boolean; detectedPath?: string | null; enabled: boolean; machineStatus: ProviderMachineStatus; lastDetection?: MachineProviderCheckResult; lastVerification?: MachineProviderCheckResult }> {
794
810
  return this.getAll().map((provider) => {
795
- const availability = this.providerAvailability.get(provider.type);
811
+ const availability = this.getEffectiveProviderAvailability(provider.type);
796
812
  const enabled = this.isMachineProviderEnabled(provider.type);
797
813
  const machineConfig = this.getMachineProviderConfig(provider.type);
798
814
  return {