@adhdev/daemon-core 0.8.24 → 0.8.27
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/agent-stream/provider-adapter.d.ts +1 -0
- package/dist/boot/daemon-lifecycle.d.ts +2 -0
- package/dist/cli-adapters/pty-transport.d.ts +3 -0
- package/dist/commands/router.d.ts +24 -0
- package/dist/index.js +309 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +309 -19
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/providers/extension-provider-instance.d.ts +7 -0
- package/dist/sessions/reconcile.d.ts +22 -0
- package/dist/status/normalize.js +14 -2
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +14 -2
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/snapshot.d.ts +0 -2
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +72 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +72 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/agent-stream/poller.ts +4 -0
- package/src/agent-stream/provider-adapter.ts +30 -1
- package/src/boot/daemon-lifecycle.ts +3 -0
- package/src/cli-adapters/pty-transport.ts +3 -0
- package/src/cli-adapters/session-host-transport.ts +8 -0
- package/src/commands/chat-commands.ts +2 -2
- package/src/commands/handler.ts +79 -13
- package/src/commands/router.ts +132 -0
- package/src/providers/cli-provider-instance.ts +29 -1
- package/src/providers/extension-provider-instance.ts +24 -1
- package/src/sessions/reconcile.ts +85 -0
- package/src/status/normalize.ts +19 -2
- package/src/status/snapshot.ts +6 -13
|
@@ -16,6 +16,7 @@ import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
|
16
16
|
import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
17
17
|
import { registerExtensionProviders } from '../cdp/setup.js';
|
|
18
18
|
import type { SessionRegistry } from '../sessions/registry.js';
|
|
19
|
+
import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
|
|
19
20
|
import { LOG } from '../logging/logger.js';
|
|
20
21
|
import type { AgentStreamState } from './types.js';
|
|
21
22
|
|
|
@@ -78,6 +79,9 @@ export class AgentStreamPoller {
|
|
|
78
79
|
|
|
79
80
|
if (!agentStreamManager || cdpManagers.size === 0) return;
|
|
80
81
|
|
|
82
|
+
// Defensive repair: keep SessionRegistry aligned with live IDE/extension instances.
|
|
83
|
+
reconcileIdeRuntimeSessions(instanceManager as any, sessionRegistry);
|
|
84
|
+
|
|
81
85
|
// ─── Phase 1: Refresh extension providers + IDE instance extensions ───
|
|
82
86
|
for (const [ideType, cdp] of cdpManagers) {
|
|
83
87
|
// 1a. Refresh CDP manager's extension providers from config
|
|
@@ -41,6 +41,15 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
|
|
|
41
41
|
return typeof (this.provider.scripts as any)?.[name] === 'function';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
private parseMaybeJson(raw: unknown): any {
|
|
45
|
+
if (typeof raw !== 'string') return raw;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
private summarizeRaw(raw: unknown): string {
|
|
45
54
|
try {
|
|
46
55
|
if (typeof raw === 'string') return raw.replace(/\s+/g, ' ').trim().slice(0, 240);
|
|
@@ -111,12 +120,32 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
|
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
async sendMessage(evaluate: AgentEvaluateFn, text: string): Promise<void> {
|
|
114
|
-
const
|
|
123
|
+
const params = { message: text };
|
|
124
|
+
const script = this.callScript('sendMessage', params) || this.callScript('sendMessage', text);
|
|
115
125
|
if (!script) throw new Error(`[${this.agentName}] sendMessage script not available`);
|
|
116
126
|
const result = await evaluate(script) as string;
|
|
117
127
|
if (result && typeof result === 'string' && result.startsWith('error:')) {
|
|
118
128
|
throw new Error(`[${this.agentName}] sendMessage failed: ${result}`);
|
|
119
129
|
}
|
|
130
|
+
|
|
131
|
+
const parsed = this.parseMaybeJson(result);
|
|
132
|
+
if (parsed === true) return;
|
|
133
|
+
if (typeof parsed === 'string') {
|
|
134
|
+
const normalized = parsed.trim().toLowerCase();
|
|
135
|
+
if (normalized === 'ok' || normalized === 'sent' || normalized === 'success' || normalized === 'true') {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (parsed && typeof parsed === 'object') {
|
|
140
|
+
if (parsed.sent === true || parsed.success === true || parsed.ok === true || parsed.submitted === true || parsed.dispatched === true) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (typeof parsed.error === 'string' && parsed.error.trim()) {
|
|
144
|
+
throw new Error(`[${this.agentName}] sendMessage failed: ${parsed.error}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error(`[${this.agentName}] sendMessage was not confirmed`);
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
async resolveAction(evaluate: AgentEvaluateFn, action: string, button?: string): Promise<boolean> {
|
|
@@ -12,6 +12,7 @@ import { DaemonCdpInitializer, type CdpInitializerConfig } from '../cdp/initiali
|
|
|
12
12
|
import { setupIdeInstance, type CdpSetupContext } from '../cdp/setup.js';
|
|
13
13
|
import { DaemonCommandHandler } from '../commands/handler.js';
|
|
14
14
|
import { DaemonCommandRouter, type CommandRouterDeps } from '../commands/router.js';
|
|
15
|
+
import type { SessionHostControlPlane } from '../commands/router.js';
|
|
15
16
|
import {
|
|
16
17
|
DaemonCliManager,
|
|
17
18
|
type CliTransportFactoryParams,
|
|
@@ -51,6 +52,7 @@ export interface DaemonInitConfig {
|
|
|
51
52
|
/** Router transport-specific callbacks */
|
|
52
53
|
onStatusChange?: () => void;
|
|
53
54
|
onPostChatCommand?: () => void;
|
|
55
|
+
sessionHostControl?: SessionHostControlPlane | null;
|
|
54
56
|
getCdpLogFn?: (ideType: string) => (msg: string) => void;
|
|
55
57
|
|
|
56
58
|
/** Additional callback after CDP manager created (transport-specific extras) */
|
|
@@ -245,6 +247,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
245
247
|
onIdeConnected: () => poller?.start(),
|
|
246
248
|
onStatusChange: config.onStatusChange,
|
|
247
249
|
onPostChatCommand: config.onPostChatCommand,
|
|
250
|
+
sessionHostControl: config.sessionHostControl,
|
|
248
251
|
getCdpLogFn: config.getCdpLogFn || ((ideType: string) => LOG.forComponent(`CDP:${ideType}`).asLogFn()),
|
|
249
252
|
});
|
|
250
253
|
|
|
@@ -42,6 +42,9 @@ export interface PtyRuntimeMetadata {
|
|
|
42
42
|
workspaceLabel?: string;
|
|
43
43
|
writeOwner?: PtyRuntimeWriteOwner | null;
|
|
44
44
|
attachedClients?: PtyRuntimeClientInfo[];
|
|
45
|
+
restoredFromStorage?: boolean;
|
|
46
|
+
recoveryState?: string | null;
|
|
47
|
+
recoveryError?: string | null;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export interface PtyRuntimeTransport {
|
|
@@ -305,6 +305,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
private handleEvent(event: SessionHostEvent): void {
|
|
308
|
+
if (!('sessionId' in event)) return;
|
|
308
309
|
if (event.sessionId !== this.options.runtimeId) return;
|
|
309
310
|
if ((event.type === 'session_started' || event.type === 'session_resumed') && typeof event.pid === 'number') {
|
|
310
311
|
this.currentPid = event.pid;
|
|
@@ -383,6 +384,13 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
383
384
|
type: client.type,
|
|
384
385
|
readOnly: client.readOnly,
|
|
385
386
|
})),
|
|
387
|
+
restoredFromStorage: record.meta?.restoredFromStorage === true,
|
|
388
|
+
recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
|
|
389
|
+
? String(record.meta.runtimeRecoveryState)
|
|
390
|
+
: null,
|
|
391
|
+
recoveryError: typeof record.meta?.runtimeRecoveryError === 'string'
|
|
392
|
+
? String(record.meta.runtimeRecoveryError)
|
|
393
|
+
: null,
|
|
386
394
|
};
|
|
387
395
|
}
|
|
388
396
|
|
|
@@ -303,7 +303,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
303
303
|
_log(`Extension: ${provider?.type || 'unknown_extension'}`);
|
|
304
304
|
// Method 1: provider sendMessage script via evaluateInSession
|
|
305
305
|
try {
|
|
306
|
-
const evalResult = await h.evaluateProviderScript('sendMessage', {
|
|
306
|
+
const evalResult = await h.evaluateProviderScript('sendMessage', { message: text }, 30000);
|
|
307
307
|
if (evalResult?.result) {
|
|
308
308
|
const parsed = parseMaybeJson(evalResult.result);
|
|
309
309
|
if (didProviderConfirmSend(parsed)) {
|
|
@@ -338,7 +338,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
_log(`Targeting IDE: ${getCurrentManagerKey(h)}`);
|
|
341
|
-
const sendScript = h.getProviderScript('sendMessage', {
|
|
341
|
+
const sendScript = h.getProviderScript('sendMessage', { message: text });
|
|
342
342
|
if (sendScript) {
|
|
343
343
|
try {
|
|
344
344
|
const result = await targetCdp.evaluate(sendScript, 30000);
|
package/src/commands/handler.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { DaemonAgentStreamManager } from '../agent-stream/index.js';
|
|
|
21
21
|
import { loadConfig } from '../config/config.js';
|
|
22
22
|
import { ChatHistoryWriter } from '../config/chat-history.js';
|
|
23
23
|
import type { SessionRegistry, SessionRuntimeTarget } from '../sessions/registry.js';
|
|
24
|
+
import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
|
|
24
25
|
import { LOG } from '../logging/logger.js';
|
|
25
26
|
|
|
26
27
|
// Sub-module imports
|
|
@@ -165,6 +166,7 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
165
166
|
session?: SessionRuntimeTarget;
|
|
166
167
|
managerKey?: string;
|
|
167
168
|
providerType?: string;
|
|
169
|
+
sessionLookupFailed?: boolean;
|
|
168
170
|
} = {};
|
|
169
171
|
|
|
170
172
|
constructor(ctx: CommandContext) {
|
|
@@ -213,9 +215,31 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
213
215
|
if (provider?.scripts) {
|
|
214
216
|
const fn = (provider.scripts as any)[scriptName];
|
|
215
217
|
if (typeof fn === 'function') {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
if (params && Object.keys(params).length > 0) {
|
|
219
|
+
const firstVal = Object.values(params)[0];
|
|
220
|
+
if (scriptName === 'sendMessage' && typeof firstVal === 'string') {
|
|
221
|
+
const legacyScript = fn(firstVal);
|
|
222
|
+
if (legacyScript) return legacyScript;
|
|
223
|
+
}
|
|
224
|
+
const script = fn(params);
|
|
225
|
+
if (script) {
|
|
226
|
+
const likelyLegacyObjectLeak =
|
|
227
|
+
typeof script === 'string'
|
|
228
|
+
&& script.includes('[object Object]')
|
|
229
|
+
&& typeof firstVal === 'string';
|
|
230
|
+
if (!likelyLegacyObjectLeak) return script;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (firstVal !== undefined) {
|
|
234
|
+
const legacyScript = fn(firstVal);
|
|
235
|
+
if (legacyScript) return legacyScript;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (script) return script;
|
|
239
|
+
} else {
|
|
240
|
+
const script = fn();
|
|
241
|
+
if (script) return script;
|
|
242
|
+
}
|
|
219
243
|
}
|
|
220
244
|
}
|
|
221
245
|
return null;
|
|
@@ -284,23 +308,36 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
284
308
|
return key.split('_')[0];
|
|
285
309
|
}
|
|
286
310
|
|
|
287
|
-
private resolveRoute(args: any): { session?: SessionRuntimeTarget; managerKey?: string; providerType?: string } {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
311
|
+
private resolveRoute(args: any): { session?: SessionRuntimeTarget; managerKey?: string; providerType?: string; sessionLookupFailed?: boolean } {
|
|
312
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
313
|
+
let session = targetSessionId ? this._ctx.sessionRegistry?.get(targetSessionId) : undefined;
|
|
314
|
+
if (targetSessionId && !session) {
|
|
315
|
+
reconcileIdeRuntimeSessions(this._ctx.instanceManager as any, this._ctx.sessionRegistry);
|
|
316
|
+
session = this._ctx.sessionRegistry?.get(targetSessionId);
|
|
317
|
+
}
|
|
318
|
+
const sessionLookupFailed = !!targetSessionId && !session;
|
|
319
|
+
|
|
320
|
+
const managerKey = this.extractIdeType(args, sessionLookupFailed);
|
|
321
|
+
let providerType: string | undefined;
|
|
322
|
+
|
|
323
|
+
if (!sessionLookupFailed) {
|
|
324
|
+
providerType =
|
|
325
|
+
session?.providerType
|
|
326
|
+
|| args?.agentType
|
|
327
|
+
|| args?.providerType
|
|
328
|
+
|| this.inferProviderType(managerKey);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { session, managerKey, providerType, sessionLookupFailed };
|
|
296
332
|
}
|
|
297
333
|
|
|
298
334
|
/** Extract CDP scope key from target session or explicit ideType */
|
|
299
|
-
private extractIdeType(args: any): string | undefined {
|
|
335
|
+
private extractIdeType(args: any, sessionLookupFailed = false): string | undefined {
|
|
300
336
|
if (args?.targetSessionId) {
|
|
301
337
|
const target = this._ctx.sessionRegistry?.get(args.targetSessionId);
|
|
302
338
|
if (target?.cdpManagerKey) return target.cdpManagerKey;
|
|
303
339
|
if (this._ctx.cdpManagers.has(args.targetSessionId)) return args.targetSessionId;
|
|
340
|
+
if (sessionLookupFailed) return undefined;
|
|
304
341
|
}
|
|
305
342
|
|
|
306
343
|
// Also accept explicit ideType from args (P2P input, agentType for extensions)
|
|
@@ -360,6 +397,35 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
360
397
|
const startedAt = Date.now();
|
|
361
398
|
this.logCommandStart(cmd, args);
|
|
362
399
|
|
|
400
|
+
const sessionScopedCommands = new Set([
|
|
401
|
+
'read_chat',
|
|
402
|
+
'send_chat',
|
|
403
|
+
'list_chats',
|
|
404
|
+
'new_chat',
|
|
405
|
+
'switch_chat',
|
|
406
|
+
'set_mode',
|
|
407
|
+
'change_model',
|
|
408
|
+
'set_thought_level',
|
|
409
|
+
'resolve_action',
|
|
410
|
+
'focus_session',
|
|
411
|
+
'pty_input',
|
|
412
|
+
'pty_resize',
|
|
413
|
+
'invoke_provider_script',
|
|
414
|
+
'list_extension_models',
|
|
415
|
+
'set_extension_model',
|
|
416
|
+
'list_extension_modes',
|
|
417
|
+
'set_extension_mode',
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
if (this._currentRoute.sessionLookupFailed && sessionScopedCommands.has(cmd)) {
|
|
421
|
+
const result = {
|
|
422
|
+
success: false,
|
|
423
|
+
error: `Live session not found for targetSessionId: ${String(args?.targetSessionId || '').trim() || 'unknown'}`,
|
|
424
|
+
};
|
|
425
|
+
this.logCommandEnd(cmd, result, startedAt);
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
363
429
|
// Commands without ideType CDP silently fail (prevent P2P retry spam)
|
|
364
430
|
let result: CommandResult;
|
|
365
431
|
if (!this._currentRoute.session && !this._currentRoute.managerKey && !this._currentRoute.providerType) {
|
package/src/commands/router.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { registerExtensionProviders } from '../cdp/setup.js';
|
|
|
14
14
|
import { DaemonCommandHandler } from './handler.js';
|
|
15
15
|
import { DaemonCliManager } from './cli-manager.js';
|
|
16
16
|
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
17
|
+
import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
|
|
17
18
|
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
18
19
|
import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
19
20
|
import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
|
|
@@ -35,6 +36,18 @@ import * as fs from 'fs';
|
|
|
35
36
|
|
|
36
37
|
// ─── Types ───
|
|
37
38
|
|
|
39
|
+
export interface SessionHostControlPlane {
|
|
40
|
+
getDiagnostics(payload?: { includeSessions?: boolean; limit?: number }): Promise<any>;
|
|
41
|
+
listSessions(): Promise<any[]>;
|
|
42
|
+
stopSession(sessionId: string): Promise<any>;
|
|
43
|
+
resumeSession(sessionId: string): Promise<any>;
|
|
44
|
+
restartSession(sessionId: string): Promise<any>;
|
|
45
|
+
sendSignal(sessionId: string, signal: string): Promise<any>;
|
|
46
|
+
forceDetachClient(sessionId: string, clientId: string): Promise<any>;
|
|
47
|
+
acquireWrite(payload: { sessionId: string; clientId: string; ownerType: 'agent' | 'user'; force?: boolean }): Promise<any>;
|
|
48
|
+
releaseWrite(payload: { sessionId: string; clientId: string }): Promise<any>;
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
export interface CommandRouterDeps {
|
|
39
52
|
commandHandler: DaemonCommandHandler;
|
|
40
53
|
cliManager: DaemonCliManager;
|
|
@@ -56,6 +69,8 @@ export interface CommandRouterDeps {
|
|
|
56
69
|
getCdpLogFn?: (ideType: string) => (msg: string) => void;
|
|
57
70
|
/** Package name for upgrade detection ('adhdev' or '@adhdev/daemon-standalone') */
|
|
58
71
|
packageName?: string;
|
|
72
|
+
/** Session host control plane */
|
|
73
|
+
sessionHostControl?: SessionHostControlPlane | null;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
export interface CommandRouterResult {
|
|
@@ -70,6 +85,30 @@ const CHAT_COMMANDS = [
|
|
|
70
85
|
];
|
|
71
86
|
const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
|
|
72
87
|
|
|
88
|
+
function toHostedCliRuntimeDescriptor(record: any): HostedCliRuntimeDescriptor | null {
|
|
89
|
+
if (!record || typeof record !== 'object') return null;
|
|
90
|
+
const runtimeId = typeof record.sessionId === 'string' ? record.sessionId : '';
|
|
91
|
+
const cliType = typeof record.providerType === 'string' ? record.providerType : '';
|
|
92
|
+
const workspace = typeof record.workspace === 'string' ? record.workspace : '';
|
|
93
|
+
if (!runtimeId || !cliType || !workspace) return null;
|
|
94
|
+
return {
|
|
95
|
+
runtimeId,
|
|
96
|
+
runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
|
|
97
|
+
displayName: typeof record.displayName === 'string' ? record.displayName : undefined,
|
|
98
|
+
workspaceLabel: typeof record.workspaceLabel === 'string' ? record.workspaceLabel : undefined,
|
|
99
|
+
lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle as HostedCliRuntimeDescriptor['lifecycle'] : undefined,
|
|
100
|
+
recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
|
|
101
|
+
? String(record.meta.runtimeRecoveryState)
|
|
102
|
+
: null,
|
|
103
|
+
cliType,
|
|
104
|
+
workspace,
|
|
105
|
+
cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs as string[] : [],
|
|
106
|
+
providerSessionId: typeof record.meta?.providerSessionId === 'string'
|
|
107
|
+
? String(record.meta.providerSessionId)
|
|
108
|
+
: undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
73
112
|
export class DaemonCommandRouter {
|
|
74
113
|
private deps: CommandRouterDeps;
|
|
75
114
|
|
|
@@ -159,6 +198,99 @@ export class DaemonCommandRouter {
|
|
|
159
198
|
}
|
|
160
199
|
}
|
|
161
200
|
|
|
201
|
+
case 'session_host_get_diagnostics': {
|
|
202
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
203
|
+
const diagnostics = await this.deps.sessionHostControl.getDiagnostics({
|
|
204
|
+
includeSessions: args?.includeSessions !== false,
|
|
205
|
+
limit: Number(args?.limit) || undefined,
|
|
206
|
+
});
|
|
207
|
+
return { success: true, diagnostics };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'session_host_list_sessions': {
|
|
211
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
212
|
+
const sessions = await this.deps.sessionHostControl.listSessions();
|
|
213
|
+
return { success: true, sessions };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'session_host_stop_session': {
|
|
217
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
218
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
219
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
220
|
+
const record = await this.deps.sessionHostControl.stopSession(sessionId);
|
|
221
|
+
return { success: true, record };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case 'session_host_resume_session': {
|
|
225
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
226
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
227
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
228
|
+
const record = await this.deps.sessionHostControl.resumeSession(sessionId);
|
|
229
|
+
const hosted = toHostedCliRuntimeDescriptor(record);
|
|
230
|
+
if (hosted) {
|
|
231
|
+
await this.deps.cliManager.restoreHostedSessions([hosted]);
|
|
232
|
+
}
|
|
233
|
+
return { success: true, record };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case 'session_host_restart_session': {
|
|
237
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
238
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
239
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
240
|
+
const record = await this.deps.sessionHostControl.restartSession(sessionId);
|
|
241
|
+
const hosted = toHostedCliRuntimeDescriptor(record);
|
|
242
|
+
if (hosted) {
|
|
243
|
+
await this.deps.cliManager.restoreHostedSessions([hosted]);
|
|
244
|
+
}
|
|
245
|
+
return { success: true, record };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case 'session_host_send_signal': {
|
|
249
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
250
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
251
|
+
const signal = typeof args?.signal === 'string' ? args.signal : '';
|
|
252
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
253
|
+
if (!signal) return { success: false, error: 'signal required' };
|
|
254
|
+
const record = await this.deps.sessionHostControl.sendSignal(sessionId, signal);
|
|
255
|
+
return { success: true, record };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'session_host_force_detach_client': {
|
|
259
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
260
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
261
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
262
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
263
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
264
|
+
const record = await this.deps.sessionHostControl.forceDetachClient(sessionId, clientId);
|
|
265
|
+
return { success: true, record };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'session_host_acquire_write': {
|
|
269
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
270
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
271
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
272
|
+
const ownerType = args?.ownerType === 'agent' ? 'agent' : 'user';
|
|
273
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
274
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
275
|
+
const record = await this.deps.sessionHostControl.acquireWrite({
|
|
276
|
+
sessionId,
|
|
277
|
+
clientId,
|
|
278
|
+
ownerType,
|
|
279
|
+
force: args?.force !== false,
|
|
280
|
+
});
|
|
281
|
+
return { success: true, record };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case 'session_host_release_write': {
|
|
285
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
286
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
287
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
288
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
289
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
290
|
+
const record = await this.deps.sessionHostControl.releaseWrite({ sessionId, clientId });
|
|
291
|
+
return { success: true, record };
|
|
292
|
+
}
|
|
293
|
+
|
|
162
294
|
case 'list_saved_sessions': {
|
|
163
295
|
const providerType = typeof args?.providerType === 'string'
|
|
164
296
|
? args.providerType.trim()
|
|
@@ -14,7 +14,7 @@ import type { ProviderModule } from './contracts.js';
|
|
|
14
14
|
import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
|
|
15
15
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
16
16
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
17
|
-
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
17
|
+
import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
18
18
|
import { StatusMonitor } from './status-monitor.js';
|
|
19
19
|
import { ChatHistoryWriter, readChatHistory } from '../config/chat-history.js';
|
|
20
20
|
import { LOG } from '../logging/logger.js';
|
|
@@ -133,6 +133,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
133
133
|
|
|
134
134
|
// PTY spawn
|
|
135
135
|
await this.adapter.spawn();
|
|
136
|
+
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
136
137
|
if (this.providerSessionId) {
|
|
137
138
|
const restoredHistory = readChatHistory(this.type, 0, 200, this.providerSessionId);
|
|
138
139
|
if (restoredHistory.messages.length > 0) {
|
|
@@ -246,6 +247,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
246
247
|
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
247
248
|
}
|
|
248
249
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
250
|
+
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
249
251
|
const parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
|
|
250
252
|
const controlValues = extractProviderControlValues(this.provider.controls, parsedStatus);
|
|
251
253
|
if (controlValues) {
|
|
@@ -603,6 +605,32 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
603
605
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
604
606
|
}
|
|
605
607
|
|
|
608
|
+
private maybeAppendRuntimeRecoveryMessage(runtime: PtyRuntimeMetadata | null): void {
|
|
609
|
+
if (!runtime?.restoredFromStorage || !runtime.runtimeId) return;
|
|
610
|
+
|
|
611
|
+
const recoveryState = String(runtime.recoveryState || '').trim();
|
|
612
|
+
if (!recoveryState) return;
|
|
613
|
+
|
|
614
|
+
let content = '';
|
|
615
|
+
if (recoveryState === 'auto_resumed') {
|
|
616
|
+
content = 'Session host restored this CLI after restart and reattached it from a saved snapshot.';
|
|
617
|
+
} else if (recoveryState === 'resume_failed') {
|
|
618
|
+
const errorSuffix = runtime.recoveryError ? ` Resume failed: ${runtime.recoveryError}` : '';
|
|
619
|
+
content = `Session host found this CLI after restart, but automatic resume failed.${errorSuffix}`;
|
|
620
|
+
} else if (recoveryState === 'host_restart_interrupted') {
|
|
621
|
+
content = 'Session host found this CLI in interrupted state after restart and is attempting to resume it.';
|
|
622
|
+
} else if (recoveryState === 'orphan_snapshot') {
|
|
623
|
+
content = 'Session host restored the last snapshot for this CLI, but the original runtime was not resumed automatically.';
|
|
624
|
+
} else {
|
|
625
|
+
content = `Session host restored this CLI after restart (${recoveryState}).`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
this.appendRuntimeSystemMessage(
|
|
629
|
+
content,
|
|
630
|
+
`runtime_recovery:${runtime.runtimeId}:${recoveryState}`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
606
634
|
private appendRuntimeSystemMessage(content: string, dedupKey: string, receivedAt = Date.now()): void {
|
|
607
635
|
const normalizedContent = String(content || '').trim();
|
|
608
636
|
if (!normalizedContent) return;
|
|
@@ -25,6 +25,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
25
25
|
private currentStatus: string = 'idle';
|
|
26
26
|
private agentStreams: any[] = [];
|
|
27
27
|
private messages: any[] = [];
|
|
28
|
+
private prevMessageHashes = new Map<string, number>();
|
|
28
29
|
private activeModal: any = null;
|
|
29
30
|
private currentModel: string = '';
|
|
30
31
|
private currentMode: string = '';
|
|
@@ -104,7 +105,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
104
105
|
if (event === 'stream_update') {
|
|
105
106
|
// Reflect data collected from agent-stream-manager
|
|
106
107
|
if (data?.streams) this.agentStreams = data.streams;
|
|
107
|
-
if (data?.messages) this.messages = data.messages;
|
|
108
|
+
if (data?.messages) this.messages = this.assignReceivedAt(data.messages);
|
|
108
109
|
if (data?.activeModal !== undefined) this.activeModal = data.activeModal;
|
|
109
110
|
if (data?.model) this.currentModel = data.model;
|
|
110
111
|
if (data?.mode) this.currentMode = data.mode;
|
|
@@ -132,6 +133,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
132
133
|
dispose(): void {
|
|
133
134
|
this.agentStreams = [];
|
|
134
135
|
this.messages = [];
|
|
136
|
+
this.prevMessageHashes.clear();
|
|
135
137
|
this.monitor.reset();
|
|
136
138
|
this.appliedEffectKeys.clear();
|
|
137
139
|
this.runtimeMessages = [];
|
|
@@ -315,6 +317,26 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
315
317
|
);
|
|
316
318
|
}
|
|
317
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Assign stable receivedAt to extension messages.
|
|
322
|
+
* Same pattern as IdeProviderInstance.readChat() prevByHash —
|
|
323
|
+
* preserves first-seen timestamp across polling cycles.
|
|
324
|
+
*/
|
|
325
|
+
private assignReceivedAt(messages: any[]): any[] {
|
|
326
|
+
const now = Date.now();
|
|
327
|
+
const nextHashes = new Map<string, number>();
|
|
328
|
+
|
|
329
|
+
for (const msg of messages) {
|
|
330
|
+
const hash = `${msg.role}:${(msg.content || '').slice(0, 100)}`;
|
|
331
|
+
const prevTime = this.prevMessageHashes.get(hash);
|
|
332
|
+
msg.receivedAt = prevTime || now;
|
|
333
|
+
nextHashes.set(hash, msg.receivedAt);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.prevMessageHashes = nextHashes;
|
|
337
|
+
return messages;
|
|
338
|
+
}
|
|
339
|
+
|
|
318
340
|
private mergeConversationMessages(messages: any[]): ChatMessage[] {
|
|
319
341
|
if (this.runtimeMessages.length === 0) return messages;
|
|
320
342
|
return [...messages, ...this.runtimeMessages.map((entry) => entry.message)]
|
|
@@ -382,6 +404,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
382
404
|
}
|
|
383
405
|
this.agentStreams = [];
|
|
384
406
|
this.messages = [];
|
|
407
|
+
this.prevMessageHashes.clear();
|
|
385
408
|
this.activeModal = null;
|
|
386
409
|
this.currentModel = '';
|
|
387
410
|
this.currentMode = '';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SessionRegistry, SessionRuntimeTarget } from './registry.js';
|
|
2
|
+
|
|
3
|
+
interface IdeLikeInstance {
|
|
4
|
+
category?: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
getInstanceId?: () => string;
|
|
7
|
+
getExtensionInstances?: () => Array<{
|
|
8
|
+
type?: string;
|
|
9
|
+
getInstanceId?: () => string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface InstanceManagerLike {
|
|
14
|
+
listInstanceIds(): string[];
|
|
15
|
+
getInstance(id: string): IdeLikeInstance | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function upsertSessionTarget(sessionRegistry: SessionRegistry, target: SessionRuntimeTarget): void {
|
|
19
|
+
const existing = sessionRegistry.get(target.sessionId);
|
|
20
|
+
if (
|
|
21
|
+
existing
|
|
22
|
+
&& existing.parentSessionId === target.parentSessionId
|
|
23
|
+
&& existing.providerType === target.providerType
|
|
24
|
+
&& existing.transport === target.transport
|
|
25
|
+
&& existing.cdpManagerKey === target.cdpManagerKey
|
|
26
|
+
&& existing.instanceKey === target.instanceKey
|
|
27
|
+
) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
sessionRegistry.register(target);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Rebuild missing IDE/runtime session registry entries from live ProviderInstance objects.
|
|
35
|
+
*
|
|
36
|
+
* This is a defensive repair path for cases where an IDE extension instance still exists
|
|
37
|
+
* in InstanceManager/status, but its runtime session entry has been dropped from SessionRegistry.
|
|
38
|
+
*/
|
|
39
|
+
export function reconcileIdeRuntimeSessions(
|
|
40
|
+
instanceManager: InstanceManagerLike | undefined,
|
|
41
|
+
sessionRegistry: SessionRegistry | undefined,
|
|
42
|
+
): void {
|
|
43
|
+
if (!instanceManager || !sessionRegistry) return;
|
|
44
|
+
|
|
45
|
+
for (const instanceKey of instanceManager.listInstanceIds()) {
|
|
46
|
+
if (!instanceKey.startsWith('ide:')) continue;
|
|
47
|
+
|
|
48
|
+
const ideInstance = instanceManager.getInstance(instanceKey);
|
|
49
|
+
if (!ideInstance || ideInstance.category !== 'ide' || typeof ideInstance.getInstanceId !== 'function') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const managerKey = instanceKey.slice(4);
|
|
54
|
+
const ideType = typeof ideInstance.type === 'string' && ideInstance.type.trim()
|
|
55
|
+
? ideInstance.type.trim()
|
|
56
|
+
: managerKey.split('_')[0];
|
|
57
|
+
const parentSessionId = ideInstance.getInstanceId();
|
|
58
|
+
if (!parentSessionId) continue;
|
|
59
|
+
|
|
60
|
+
upsertSessionTarget(sessionRegistry, {
|
|
61
|
+
sessionId: parentSessionId,
|
|
62
|
+
parentSessionId: null,
|
|
63
|
+
providerType: ideType,
|
|
64
|
+
transport: 'cdp-page',
|
|
65
|
+
cdpManagerKey: managerKey,
|
|
66
|
+
instanceKey,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const extensions = ideInstance.getExtensionInstances?.() || [];
|
|
70
|
+
for (const ext of extensions) {
|
|
71
|
+
const extType = typeof ext?.type === 'string' ? ext.type.trim() : '';
|
|
72
|
+
const extSessionId = ext?.getInstanceId?.();
|
|
73
|
+
if (!extType || !extSessionId) continue;
|
|
74
|
+
|
|
75
|
+
upsertSessionTarget(sessionRegistry, {
|
|
76
|
+
sessionId: extSessionId,
|
|
77
|
+
parentSessionId,
|
|
78
|
+
providerType: extType,
|
|
79
|
+
transport: 'cdp-webview',
|
|
80
|
+
cdpManagerKey: managerKey,
|
|
81
|
+
instanceKey,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|