@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.
- package/dist/commands/upgrade-helper.d.ts +1 -0
- package/dist/index.js +200 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +200 -20
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/providers/provider-instance-manager.d.ts +2 -1
- package/dist/providers/provider-instance.d.ts +17 -0
- package/dist/providers/provider-loader.d.ts +1 -0
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +46 -3
- package/src/cli-adapters/provider-cli-parse.ts +3 -2
- package/src/commands/chat-commands.ts +54 -0
- package/src/commands/upgrade-helper.ts +48 -16
- package/src/providers/cli-provider-instance.ts +25 -1
- package/src/providers/provider-instance-manager.ts +43 -1
- package/src/providers/provider-instance.ts +19 -0
- package/src/providers/provider-loader.ts +18 -2
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1067,7 +1067,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1067
1067
|
}
|
|
1068
1068
|
|
|
1069
1069
|
if (this.currentTurnScope && !lastParsedAssistant) {
|
|
1070
|
-
LOG.
|
|
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
|
-
|
|
2018
|
-
|
|
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
|
-
&&
|
|
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 =
|
|
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.
|
|
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.
|
|
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 {
|