@adhdev/daemon-core 0.8.25 → 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 +221 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +221 -14
- 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/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/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 +25 -3
- 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/status/normalize.ts +19 -2
- package/src/status/snapshot.ts +6 -13
|
@@ -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
|
@@ -215,9 +215,31 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
215
215
|
if (provider?.scripts) {
|
|
216
216
|
const fn = (provider.scripts as any)[scriptName];
|
|
217
217
|
if (typeof fn === 'function') {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
}
|
|
221
243
|
}
|
|
222
244
|
}
|
|
223
245
|
return null;
|
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 = '';
|
package/src/status/normalize.ts
CHANGED
|
@@ -64,6 +64,23 @@ function trimMessageForStatus(message: unknown, stringLimit: number): unknown {
|
|
|
64
64
|
return trimStructuredStrings(message, stringLimit);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Collapse timestamp / createdAt into receivedAt so downstream consumers
|
|
69
|
+
* only ever need to read a single canonical time field.
|
|
70
|
+
*/
|
|
71
|
+
function normalizeMessageTime(message: unknown): unknown {
|
|
72
|
+
if (!message || typeof message !== 'object') return message;
|
|
73
|
+
const msg = message as Record<string, unknown>;
|
|
74
|
+
if (msg.receivedAt == null) {
|
|
75
|
+
const fallback = msg.timestamp ?? msg.createdAt;
|
|
76
|
+
if (fallback != null) {
|
|
77
|
+
const ts = typeof fallback === 'string' ? Date.parse(fallback as string) : Number(fallback);
|
|
78
|
+
if (Number.isFinite(ts) && ts > 0) msg.receivedAt = ts;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return msg;
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[] {
|
|
68
85
|
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
69
86
|
|
|
@@ -72,11 +89,11 @@ function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[
|
|
|
72
89
|
let totalBytes = 0;
|
|
73
90
|
|
|
74
91
|
for (let i = recent.length - 1; i >= 0; i -= 1) {
|
|
75
|
-
let normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT);
|
|
92
|
+
let normalized = normalizeMessageTime(trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT));
|
|
76
93
|
let size = estimateBytes(normalized);
|
|
77
94
|
|
|
78
95
|
if (size > STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT) {
|
|
79
|
-
normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT);
|
|
96
|
+
normalized = normalizeMessageTime(trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT));
|
|
80
97
|
size = estimateBytes(normalized);
|
|
81
98
|
}
|
|
82
99
|
|
package/src/status/snapshot.ts
CHANGED
|
@@ -95,17 +95,12 @@ function parseMessageTime(value: unknown): number {
|
|
|
95
95
|
|
|
96
96
|
function getSessionMessageUpdatedAt(session: {
|
|
97
97
|
activeChat?: {
|
|
98
|
-
messages?: Array<{
|
|
98
|
+
messages?: Array<{ receivedAt?: number | string }> | null
|
|
99
99
|
} | null
|
|
100
100
|
}) {
|
|
101
101
|
const lastMessage = session.activeChat?.messages?.at?.(-1);
|
|
102
102
|
if (!lastMessage) return 0;
|
|
103
|
-
return (
|
|
104
|
-
parseMessageTime(lastMessage.timestamp)
|
|
105
|
-
|| parseMessageTime(lastMessage.receivedAt)
|
|
106
|
-
|| parseMessageTime(lastMessage.createdAt)
|
|
107
|
-
|| 0
|
|
108
|
-
);
|
|
103
|
+
return parseMessageTime(lastMessage.receivedAt) || 0;
|
|
109
104
|
}
|
|
110
105
|
|
|
111
106
|
export function getSessionCompletionMarker(session: {
|
|
@@ -114,9 +109,7 @@ export function getSessionCompletionMarker(session: {
|
|
|
114
109
|
role?: string;
|
|
115
110
|
id?: string;
|
|
116
111
|
index?: number;
|
|
117
|
-
timestamp?: number | string;
|
|
118
112
|
receivedAt?: number | string;
|
|
119
|
-
createdAt?: number | string;
|
|
120
113
|
_turnKey?: string;
|
|
121
114
|
}> | null
|
|
122
115
|
} | null
|
|
@@ -124,17 +117,17 @@ export function getSessionCompletionMarker(session: {
|
|
|
124
117
|
const lastMessage = session.activeChat?.messages?.at?.(-1) as any;
|
|
125
118
|
if (!lastMessage) return '';
|
|
126
119
|
const role = typeof lastMessage.role === 'string' ? lastMessage.role : '';
|
|
127
|
-
if (role === 'user' || role === 'human') return '';
|
|
120
|
+
if (role === 'user' || role === 'human' || role === 'system') return '';
|
|
128
121
|
if (typeof lastMessage._turnKey === 'string' && lastMessage._turnKey) return `turn:${lastMessage._turnKey}`;
|
|
129
122
|
if (typeof lastMessage.id === 'string' && lastMessage.id) return `id:${lastMessage.id}`;
|
|
130
123
|
if (typeof lastMessage.index === 'number' && Number.isFinite(lastMessage.index)) return `idx:${lastMessage.index}`;
|
|
131
|
-
const timestamp = parseMessageTime(lastMessage.
|
|
124
|
+
const timestamp = parseMessageTime(lastMessage.receivedAt);
|
|
132
125
|
return timestamp > 0 ? `ts:${timestamp}` : '';
|
|
133
126
|
}
|
|
134
127
|
|
|
135
128
|
function getSessionLastUsedAt(session: {
|
|
136
129
|
activeChat?: {
|
|
137
|
-
messages?: Array<{
|
|
130
|
+
messages?: Array<{ receivedAt?: number | string }> | null
|
|
138
131
|
} | null
|
|
139
132
|
lastUpdated?: number
|
|
140
133
|
}) {
|
|
@@ -171,7 +164,7 @@ function getUnreadState(
|
|
|
171
164
|
}
|
|
172
165
|
const unread = completionMarker
|
|
173
166
|
? completionMarker !== seenCompletionMarker
|
|
174
|
-
: hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human';
|
|
167
|
+
: hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human' && lastRole !== 'system';
|
|
175
168
|
return { unread, inboxBucket: unread ? 'task_complete' : 'idle' };
|
|
176
169
|
}
|
|
177
170
|
|