@adhdev/daemon-core 0.7.45 → 0.8.0

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.
Files changed (54) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +34 -0
  2. package/dist/cli-adapters/pty-transport.d.ts +1 -0
  3. package/dist/cli-adapters/session-host-transport.d.ts +1 -0
  4. package/dist/commands/cli-manager.d.ts +11 -2
  5. package/dist/config/chat-history.d.ts +32 -2
  6. package/dist/config/config.d.ts +5 -1
  7. package/dist/config/recent-activity.d.ts +3 -1
  8. package/dist/config/saved-sessions.d.ts +22 -0
  9. package/dist/daemon/dev-auto-implement.d.ts +18 -2
  10. package/dist/daemon/dev-cli-debug.d.ts +82 -0
  11. package/dist/daemon/dev-server.d.ts +7 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +6122 -4038
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +6114 -4032
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/providers/cli-provider-instance.d.ts +29 -1
  18. package/dist/providers/contracts.d.ts +11 -0
  19. package/dist/providers/provider-instance.d.ts +1 -0
  20. package/dist/shared-types.d.ts +2 -0
  21. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
  22. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
  23. package/node_modules/@adhdev/session-host-core/dist/index.js +9 -0
  24. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  25. package/node_modules/@adhdev/session-host-core/dist/index.mjs +9 -0
  26. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  27. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  28. package/package.json +1 -1
  29. package/src/boot/daemon-lifecycle.ts +19 -15
  30. package/src/cli-adapters/provider-cli-adapter.ts +424 -7
  31. package/src/cli-adapters/pty-transport.ts +1 -0
  32. package/src/cli-adapters/session-host-transport.ts +32 -1
  33. package/src/commands/chat-commands.ts +36 -8
  34. package/src/commands/cli-manager.ts +259 -22
  35. package/src/commands/router.ts +52 -1
  36. package/src/config/chat-history.ts +197 -10
  37. package/src/config/config.d.ts +4 -0
  38. package/src/config/config.ts +8 -2
  39. package/src/config/recent-activity.ts +13 -2
  40. package/src/config/saved-sessions.ts +73 -0
  41. package/src/daemon/dev-auto-implement.ts +394 -43
  42. package/src/daemon/dev-cli-debug.ts +839 -0
  43. package/src/daemon/dev-server.ts +51 -5
  44. package/src/index.ts +2 -0
  45. package/src/providers/cli-provider-instance.ts +283 -4
  46. package/src/providers/contracts.ts +11 -0
  47. package/src/providers/provider-instance.d.ts +1 -0
  48. package/src/providers/provider-instance.ts +1 -0
  49. package/src/providers/provider-loader.ts +39 -0
  50. package/src/session-host/runtime-support.ts +1 -0
  51. package/src/shared-types.d.ts +2 -0
  52. package/src/shared-types.ts +2 -0
  53. package/src/status/builders.ts +1 -0
  54. package/src/status/snapshot.ts +1 -0
@@ -43,6 +43,7 @@ export interface PtyRuntimeTransport {
43
43
  kill(): void;
44
44
  clearBuffer?(): void;
45
45
  detach?(): void;
46
+ updateMeta?(meta: Record<string, unknown>, replace?: boolean): void;
46
47
  getMetadata?(): PtyRuntimeMetadata | null;
47
48
  onData(callback: (data: string) => void): void;
48
49
  onExit(callback: (info: { exitCode: number }) => void): void;
@@ -11,6 +11,7 @@ import type { PtyRuntimeMetadata, PtyRuntimeTransport, PtySpawnOptions, PtyTrans
11
11
  interface SessionHostPtyTransportFactoryOptions {
12
12
  endpoint?: SessionHostEndpoint;
13
13
  appName?: string;
14
+ ensureReady?: () => Promise<void>;
14
15
  clientId: string;
15
16
  runtimeId: string;
16
17
  providerType: string;
@@ -171,8 +172,38 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
171
172
  });
172
173
  }
173
174
 
175
+ updateMeta(meta: Record<string, unknown>, replace = false): void {
176
+ this.enqueue(async () => {
177
+ const response = await this.client.request<SessionHostRecord>({
178
+ type: 'update_session_meta',
179
+ payload: {
180
+ sessionId: this.options.runtimeId,
181
+ meta,
182
+ replace,
183
+ },
184
+ });
185
+ if (!response?.success) {
186
+ throw new Error(response.error || `Failed to update runtime meta ${this.options.runtimeId}`);
187
+ }
188
+ if (response.result) {
189
+ this.updateMetadata(response.result);
190
+ }
191
+ });
192
+ }
193
+
174
194
  private async boot(): Promise<void> {
175
- await this.client.connect();
195
+ if (typeof this.options.ensureReady === 'function') {
196
+ await this.options.ensureReady();
197
+ }
198
+ try {
199
+ await this.client.connect();
200
+ } catch (error) {
201
+ if (typeof this.options.ensureReady !== 'function') {
202
+ throw error;
203
+ }
204
+ await this.options.ensureReady();
205
+ await this.client.connect();
206
+ }
176
207
  this.unsubscribe = this.client.onEvent((event: SessionHostEvent) => this.handleEvent(event));
177
208
 
178
209
  let record: SessionHostRecord | null = null;
@@ -23,6 +23,13 @@ function getTargetedCliAdapter(h: CommandHelpers, args: any, providerType?: stri
23
23
  return h.getCliAdapter(args?.targetSessionId || providerType || h.currentSession?.providerType || h.currentManagerKey);
24
24
  }
25
25
 
26
+ function getTargetInstance(h: CommandHelpers, args: any) {
27
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
28
+ const sessionId = targetSessionId || h.currentSession?.sessionId || '';
29
+ if (!sessionId) return null;
30
+ return h.ctx.instanceManager?.getInstance(sessionId) as any;
31
+ }
32
+
26
33
  function getTargetTransport(h: CommandHelpers, provider?: any): SessionTransport | null {
27
34
  if (h.currentSession?.transport) return h.currentSession.transport;
28
35
  switch (provider?.category) {
@@ -59,6 +66,19 @@ function buildRecentSendKey(h: CommandHelpers, args: any, provider: any, text: s
59
66
  return `${transport}:${target}:${text.trim()}`;
60
67
  }
61
68
 
69
+ function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
70
+ const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
71
+ if (explicit) return explicit;
72
+
73
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
74
+ if (!targetSessionId) return undefined;
75
+
76
+ const instance = h.ctx.instanceManager?.getInstance(targetSessionId) as any;
77
+ const state = instance?.getState?.();
78
+ const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
79
+ return providerSessionId || targetSessionId;
80
+ }
81
+
62
82
  function isRecentDuplicateSend(key: string): boolean {
63
83
  const now = Date.now();
64
84
  for (const [candidate, ts] of recentSendByTarget.entries()) {
@@ -72,11 +92,11 @@ function isRecentDuplicateSend(key: string): boolean {
72
92
 
73
93
  export async function handleChatHistory(h: CommandHelpers, args: any): Promise<CommandResult> {
74
94
  const { agentType, offset, limit } = args;
75
- const instanceId = args?.targetSessionId;
95
+ const historySessionId = getHistorySessionId(h, args);
76
96
  try {
77
97
  const provider = h.getProvider(agentType);
78
98
  const agentStr = provider?.type || agentType || getCurrentProviderType(h);
79
- const result = readChatHistory(agentStr, offset || 0, limit || 30, instanceId);
99
+ const result = readChatHistory(agentStr, offset || 0, limit || 30, historySessionId);
80
100
  return { success: true, ...result, agent: agentStr };
81
101
  } catch (e: any) {
82
102
  return { success: false, error: e.message };
@@ -86,6 +106,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
86
106
  export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
87
107
  const provider = h.getProvider(args?.agentType);
88
108
  const transport = getTargetTransport(h, provider);
109
+ const historySessionId = getHistorySessionId(h, args);
89
110
 
90
111
  const _log = (msg: string) => LOG.debug('Command', `[read_chat] ${msg}`);
91
112
 
@@ -120,7 +141,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
120
141
  provider?.type || 'unknown_extension',
121
142
  parsed.messages || [],
122
143
  parsed.title,
123
- args?.targetSessionId
144
+ args?.targetSessionId,
145
+ historySessionId,
124
146
  );
125
147
  return { success: true, ...parsed };
126
148
  }
@@ -142,7 +164,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
142
164
  stream.agentType,
143
165
  stream.messages || [],
144
166
  undefined,
145
- args?.targetSessionId
167
+ args?.targetSessionId,
168
+ historySessionId,
146
169
  );
147
170
  return { success: true, messages: stream.messages || [], status: stream.status, agentType: stream.agentType };
148
171
  }
@@ -169,11 +192,12 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
169
192
  if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
170
193
  if (parsed && typeof parsed === 'object') {
171
194
  _log(`Webview OK: ${parsed.messages?.length || 0} msgs`);
172
- h.historyWriter.appendNewMessages(
195
+ h.historyWriter.appendNewMessages(
173
196
  provider?.type || getCurrentProviderType(h, 'unknown_webview'),
174
197
  parsed.messages || [],
175
198
  parsed.title,
176
- args?.targetSessionId
199
+ args?.targetSessionId,
200
+ historySessionId,
177
201
  );
178
202
  return { success: true, ...parsed };
179
203
  }
@@ -197,7 +221,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
197
221
  provider?.type || getCurrentProviderType(h, 'unknown_ide'),
198
222
  parsed.messages || [],
199
223
  parsed.title,
200
- args?.targetSessionId
224
+ args?.targetSessionId,
225
+ historySessionId,
201
226
  );
202
227
  return { success: true, ...parsed };
203
228
  }
@@ -216,13 +241,15 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
216
241
  const provider = h.getProvider(args?.agentType);
217
242
  const transport = getTargetTransport(h, provider);
218
243
  const dedupeKey = buildRecentSendKey(h, args, provider, text);
244
+ const historySessionId = getHistorySessionId(h, args);
219
245
 
220
246
  const _logSendSuccess = (method: string, targetAgent?: string) => {
221
247
  h.historyWriter.appendNewMessages(
222
248
  targetAgent || provider?.type || getCurrentProviderType(h, 'unknown_agent'),
223
249
  [{ role: 'user', content: text, receivedAt: Date.now() }],
224
250
  undefined, // title
225
- args?.targetSessionId
251
+ args?.targetSessionId,
252
+ historySessionId,
226
253
  );
227
254
  return { success: true, sent: true, method, targetAgent };
228
255
  };
@@ -761,6 +788,7 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
761
788
  (adapter as any).writeRaw?.(keys);
762
789
  }
763
790
  LOG.info('Command', `[resolveAction] CLI PTY → buttonIndex=${buttonIndex} "${buttons[buttonIndex] ?? '?'}"`);
791
+ getTargetInstance(h, args)?.recordApprovalSelection?.(buttons[buttonIndex] ?? button);
764
792
  return { success: true, buttonIndex, button: buttons[buttonIndex] ?? button };
765
793
  }
766
794
 
@@ -14,10 +14,12 @@ import { detectCLI } from '../detection/cli-detector.js';
14
14
  import { loadConfig, saveConfig } from '../config/config.js';
15
15
  import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
16
16
  import { appendRecentActivity } from '../config/recent-activity.js';
17
+ import { upsertSavedProviderSession } from '../config/saved-sessions.js';
17
18
  import { CliProviderInstance } from '../providers/cli-provider-instance.js';
18
19
  import { AcpProviderInstance } from '../providers/acp-provider-instance.js';
19
20
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
20
21
  import { ProviderLoader } from '../providers/provider-loader.js';
22
+ import type { ProviderModule, ProviderResumeCapability } from '../providers/contracts.js';
21
23
  import type { CliAdapter } from '../cli-adapter-types.js';
22
24
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
23
25
  import type { SessionRegistry } from '../sessions/registry.js';
@@ -29,7 +31,7 @@ export interface CliManagerDeps {
29
31
  /** Server connection — injected into adapter */
30
32
  getServerConn(): any | null;
31
33
  /** P2P — PTY output transmit */
32
- getP2p(): { broadcastPtyOutput(key: string, data: string): void } | null;
34
+ getP2p(): { broadcastSessionOutput(key: string, data: string): void } | null;
33
35
  /** StatusReporter callback */
34
36
  onStatusChange(): void;
35
37
  removeAgentTracking(key: string): void;
@@ -47,6 +49,7 @@ export interface CliTransportFactoryParams {
47
49
  providerType: string;
48
50
  workspace: string;
49
51
  cliArgs?: string[];
52
+ providerSessionId?: string;
50
53
  attachExisting?: boolean;
51
54
  }
52
55
 
@@ -60,6 +63,7 @@ export interface HostedCliRuntimeDescriptor {
60
63
  cliType: string;
61
64
  workspace: string;
62
65
  cliArgs?: string[];
66
+ providerSessionId?: string;
63
67
  }
64
68
 
65
69
  const chalkApi: any = (chalk as any)?.yellow
@@ -71,6 +75,148 @@ function colorize(color: 'red' | 'green' | 'yellow' | 'cyan', text: string): str
71
75
  return typeof fn === 'function' ? fn(text) : text;
72
76
  }
73
77
 
78
+ type CliLaunchMode = 'new' | 'resume' | 'manual';
79
+
80
+ type CliSessionBinding = {
81
+ cliArgs?: string[];
82
+ providerSessionId?: string;
83
+ launchMode: CliLaunchMode;
84
+ };
85
+
86
+ function isUuid(value: string): boolean {
87
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
88
+ }
89
+
90
+ function readArgValue(args: string[], flags: string[]): string | undefined {
91
+ for (let index = 0; index < args.length; index += 1) {
92
+ const arg = args[index];
93
+ for (const flag of flags) {
94
+ if (arg === flag) {
95
+ const next = args[index + 1];
96
+ if (next && !next.startsWith('-')) return next;
97
+ }
98
+ const prefix = `${flag}=`;
99
+ if (arg.startsWith(prefix)) return arg.slice(prefix.length);
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ function hasArg(args: string[], flags: string[]): boolean {
106
+ return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
107
+ }
108
+
109
+ function expandResumeArgs(template: string[] | undefined, sessionId: string): string[] | undefined {
110
+ if (!Array.isArray(template) || template.length === 0) return undefined;
111
+ return template.map((part) => part === '{{id}}' ? sessionId : part);
112
+ }
113
+
114
+ function readCodexResumeSessionId(args: string[]): string | undefined {
115
+ const resumeIndex = args.findIndex((arg) => arg === 'resume' || arg === 'fork');
116
+ if (resumeIndex < 0) return undefined;
117
+ const candidate = args[resumeIndex + 1];
118
+ if (!candidate || candidate.startsWith('-')) return undefined;
119
+ return candidate;
120
+ }
121
+
122
+ function detectExplicitProviderSessionId(
123
+ normalizedType: string,
124
+ args: string[],
125
+ ): { providerSessionId?: string; launchMode: CliLaunchMode } {
126
+ const explicitResumeId = readArgValue(args, ['--resume', '-r']);
127
+ if (explicitResumeId) {
128
+ return { providerSessionId: explicitResumeId, launchMode: 'resume' };
129
+ }
130
+
131
+ const explicitSessionFlagId = readArgValue(args, ['--session']);
132
+ if (explicitSessionFlagId) {
133
+ return {
134
+ providerSessionId: explicitSessionFlagId,
135
+ launchMode: 'resume',
136
+ };
137
+ }
138
+
139
+ const explicitSessionId = readArgValue(args, ['--session-id']);
140
+ if (explicitSessionId) {
141
+ if (normalizedType === 'goose-cli' && !hasArg(args, ['--resume', '-r'])) {
142
+ return { launchMode: 'manual' };
143
+ }
144
+ const isResume = normalizedType === 'goose-cli'
145
+ ? hasArg(args, ['--resume', '-r'])
146
+ : (hasArg(args, ['--continue']) || hasArg(args, ['--resume', '-r']));
147
+ return {
148
+ providerSessionId: explicitSessionId,
149
+ launchMode: isResume ? 'resume' : 'new',
150
+ };
151
+ }
152
+
153
+ if (normalizedType === 'codex-cli') {
154
+ const codexSessionId = readCodexResumeSessionId(args);
155
+ if (codexSessionId) {
156
+ return { providerSessionId: codexSessionId, launchMode: 'resume' };
157
+ }
158
+ }
159
+
160
+ return { launchMode: 'manual' };
161
+ }
162
+
163
+ export function supportsExplicitSessionResume(resume?: ProviderResumeCapability): boolean {
164
+ return !!(resume?.supported && Array.isArray(resume.resumeSessionArgs) && resume.resumeSessionArgs.length > 0);
165
+ }
166
+
167
+ function supportsExplicitSessionStart(resume?: ProviderResumeCapability): boolean {
168
+ return !!(resume?.supported && Array.isArray(resume.newSessionArgs) && resume.newSessionArgs.length > 0);
169
+ }
170
+
171
+ function resolveCliSessionBinding(
172
+ provider: ProviderModule | undefined,
173
+ normalizedType: string,
174
+ cliArgs?: string[],
175
+ requestedResumeSessionId?: string,
176
+ ): CliSessionBinding {
177
+ const baseArgs = Array.isArray(cliArgs) ? [...cliArgs] : undefined;
178
+ const resume = provider?.resume;
179
+ if (!resume?.supported) {
180
+ return { cliArgs: baseArgs, launchMode: 'manual' };
181
+ }
182
+
183
+ const explicit = detectExplicitProviderSessionId(normalizedType, baseArgs || []);
184
+ if (explicit.providerSessionId) {
185
+ return {
186
+ cliArgs: baseArgs,
187
+ providerSessionId: explicit.providerSessionId,
188
+ launchMode: explicit.launchMode,
189
+ };
190
+ }
191
+
192
+ if (requestedResumeSessionId) {
193
+ if (resume.sessionIdFormat === 'uuid' && !isUuid(requestedResumeSessionId)) {
194
+ throw new Error(`Invalid ${provider?.displayName || provider?.name || normalizedType} session ID: ${requestedResumeSessionId}`);
195
+ }
196
+ const resumeSessionArgs = expandResumeArgs(resume.resumeSessionArgs, requestedResumeSessionId);
197
+ if (!resumeSessionArgs) {
198
+ return { cliArgs: baseArgs, launchMode: 'manual' };
199
+ }
200
+ return {
201
+ cliArgs: [...(baseArgs || []), ...resumeSessionArgs],
202
+ providerSessionId: requestedResumeSessionId,
203
+ launchMode: 'resume',
204
+ };
205
+ }
206
+
207
+ if (!supportsExplicitSessionStart(resume)) {
208
+ return { cliArgs: baseArgs, launchMode: 'manual' };
209
+ }
210
+
211
+ const providerSessionId = crypto.randomUUID();
212
+ const newSessionArgs = expandResumeArgs(resume.newSessionArgs, providerSessionId);
213
+ return {
214
+ cliArgs: [...(baseArgs || []), ...(newSessionArgs || [])],
215
+ providerSessionId,
216
+ launchMode: 'new',
217
+ };
218
+ }
219
+
74
220
  // ─── DaemonCliManager ────────────────────────────
75
221
 
76
222
  export class DaemonCliManager {
@@ -107,13 +253,26 @@ export class DaemonCliManager {
107
253
  kind: 'ide' | 'cli' | 'acp';
108
254
  providerType: string;
109
255
  providerName: string;
256
+ providerSessionId?: string;
110
257
  workspace?: string;
111
258
  currentModel?: string;
112
259
  sessionId?: string;
113
260
  title?: string;
114
261
  }): void {
115
262
  try {
116
- saveConfig(appendRecentActivity(loadConfig(), entry));
263
+ let nextConfig = appendRecentActivity(loadConfig(), entry);
264
+ if (entry.providerSessionId && (entry.kind === 'cli' || entry.kind === 'acp')) {
265
+ nextConfig = upsertSavedProviderSession(nextConfig, {
266
+ kind: entry.kind,
267
+ providerType: entry.providerType,
268
+ providerName: entry.providerName,
269
+ providerSessionId: entry.providerSessionId,
270
+ workspace: entry.workspace,
271
+ currentModel: entry.currentModel,
272
+ title: entry.title,
273
+ });
274
+ }
275
+ saveConfig(nextConfig);
117
276
  } catch (e) {
118
277
  console.error(colorize('red', ` ✗ Failed to save recent activity: ${e}`));
119
278
  }
@@ -124,6 +283,7 @@ export class DaemonCliManager {
124
283
  providerType: string,
125
284
  workspace: string,
126
285
  cliArgs?: string[],
286
+ providerSessionId?: string,
127
287
  attachExisting = false,
128
288
  ): PtyTransportFactory | undefined {
129
289
  return this.deps.createPtyTransportFactory?.({
@@ -131,6 +291,7 @@ export class DaemonCliManager {
131
291
  providerType,
132
292
  workspace,
133
293
  cliArgs,
294
+ providerSessionId,
134
295
  attachExisting,
135
296
  }) || undefined;
136
297
  }
@@ -140,6 +301,7 @@ export class DaemonCliManager {
140
301
  workingDir: string,
141
302
  cliArgs: string[] | undefined,
142
303
  runtimeId: string,
304
+ providerSessionId?: string,
143
305
  attachExisting = false,
144
306
  ): CliAdapter {
145
307
  // cliType normalize (Resolve alias)
@@ -150,7 +312,14 @@ export class DaemonCliManager {
150
312
  if (provider && provider.category === 'cli' && provider.patterns && provider.spawn) {
151
313
  console.log(colorize('cyan', ` 📦 Using provider: ${provider.name} (${provider.type})`));
152
314
  const resolvedProvider = this.providerLoader.resolve(normalizedType) || provider;
153
- const transportFactory = this.getTransportFactory(runtimeId, normalizedType, workingDir, cliArgs, attachExisting);
315
+ const transportFactory = this.getTransportFactory(
316
+ runtimeId,
317
+ normalizedType,
318
+ workingDir,
319
+ cliArgs,
320
+ providerSessionId,
321
+ attachExisting,
322
+ );
154
323
  return new ProviderCliAdapter(resolvedProvider as any, workingDir, cliArgs, transportFactory);
155
324
  }
156
325
 
@@ -191,18 +360,37 @@ export class DaemonCliManager {
191
360
  provider: any,
192
361
  settings: Record<string, any>,
193
362
  attachExisting = false,
363
+ options?: {
364
+ providerSessionId?: string;
365
+ launchMode?: CliLaunchMode;
366
+ onProviderSessionResolved?: (info: {
367
+ instanceId: string;
368
+ providerType: string;
369
+ providerName: string;
370
+ workspace: string;
371
+ providerSessionId: string;
372
+ previousProviderSessionId?: string;
373
+ }) => void;
374
+ },
194
375
  ): Promise<void> {
195
376
  const instanceManager = this.deps.getInstanceManager();
196
377
  const sessionRegistry = this.deps.getSessionRegistry?.() || null;
197
378
  if (!instanceManager) throw new Error('InstanceManager not available');
198
- const transportFactory = this.getTransportFactory(key, normalizedType, resolvedDir, cliArgs, attachExisting);
199
- const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key, transportFactory);
379
+ const transportFactory = this.getTransportFactory(
380
+ key,
381
+ normalizedType,
382
+ resolvedDir,
383
+ cliArgs,
384
+ options?.providerSessionId,
385
+ attachExisting,
386
+ );
387
+ const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key, transportFactory, options);
200
388
  try {
201
389
  await instanceManager.addInstance(key, cliInstance, {
202
390
  serverConn: this.deps.getServerConn(),
203
391
  settings,
204
392
  onPtyData: (data: string) => {
205
- this.deps.getP2p()?.broadcastPtyOutput(cliInstance.instanceId, data);
393
+ this.deps.getP2p()?.broadcastSessionOutput(cliInstance.instanceId, data);
206
394
  },
207
395
  });
208
396
  sessionRegistry?.register({
@@ -225,7 +413,13 @@ export class DaemonCliManager {
225
413
 
226
414
  // ─── Session start/management ──────────────────────────────
227
415
 
228
- async startSession(cliType: string, workingDir: string, cliArgs?: string[], initialModel?: string): Promise<void> {
416
+ async startSession(
417
+ cliType: string,
418
+ workingDir: string,
419
+ cliArgs?: string[],
420
+ initialModel?: string,
421
+ options?: { resumeSessionId?: string },
422
+ ): Promise<{ runtimeSessionId: string; providerSessionId?: string }> {
229
423
  const trimmed = (workingDir || '').trim();
230
424
  if (!trimmed) throw new Error('working directory required');
231
425
  const resolvedDir = trimmed.startsWith('~')
@@ -319,7 +513,7 @@ export class DaemonCliManager {
319
513
  title: provider.displayName || provider.name || normalizedType,
320
514
  });
321
515
  this.deps.onStatusChange();
322
- return;
516
+ return { runtimeSessionId: sessionId };
323
517
  }
324
518
 
325
519
  // ─── CLI category handling (existing) ───
@@ -331,8 +525,9 @@ export class DaemonCliManager {
331
525
  console.log(colorize('cyan', ` 📦 Using provider: ${provider.name} (${provider.type})`));
332
526
  }
333
527
 
334
- // ─── Resolve launch options → extra args ───
335
- const resolvedCliArgs = cliArgs;
528
+ // ─── Resolve launch options → provider session binding ───
529
+ const sessionBinding = resolveCliSessionBinding(provider, normalizedType, cliArgs, options?.resumeSessionId);
530
+ const resolvedCliArgs = sessionBinding.cliArgs;
336
531
 
337
532
  // If InstanceManager exists, manage as CliProviderInstance unified
338
533
  const instanceManager = this.deps.getInstanceManager();
@@ -347,11 +542,32 @@ export class DaemonCliManager {
347
542
  resolvedProvider,
348
543
  {},
349
544
  false,
545
+ {
546
+ providerSessionId: sessionBinding.providerSessionId,
547
+ launchMode: sessionBinding.launchMode,
548
+ onProviderSessionResolved: ({ providerSessionId, providerName, providerType, workspace }) => {
549
+ this.persistRecentActivity({
550
+ kind: 'cli',
551
+ providerType,
552
+ providerName,
553
+ providerSessionId,
554
+ workspace,
555
+ title: providerName,
556
+ });
557
+ },
558
+ },
350
559
  );
351
560
  console.log(colorize('green', ` ✓ CLI started: ${cliInfo.displayName} v${cliInfo.version || 'unknown'} in ${resolvedDir}`));
352
561
  } else {
353
562
  // Fallback: InstanceManager without directly adapter manage
354
- const adapter = this.createAdapter(cliType, resolvedDir, resolvedCliArgs, key, false);
563
+ const adapter = this.createAdapter(
564
+ cliType,
565
+ resolvedDir,
566
+ resolvedCliArgs,
567
+ key,
568
+ sessionBinding.providerSessionId,
569
+ false,
570
+ );
355
571
  try {
356
572
  await adapter.spawn();
357
573
  } catch (spawnErr: any) {
@@ -380,7 +596,7 @@ export class DaemonCliManager {
380
596
 
381
597
  if (typeof adapter.setOnPtyData === 'function') {
382
598
  adapter.setOnPtyData((data: string) => {
383
- this.deps.getP2p()?.broadcastPtyOutput(key, data);
599
+ this.deps.getP2p()?.broadcastSessionOutput(key, data);
384
600
  });
385
601
  }
386
602
 
@@ -392,6 +608,7 @@ export class DaemonCliManager {
392
608
  kind: 'cli',
393
609
  providerType: normalizedType,
394
610
  providerName: provider?.displayName || provider?.name || normalizedType,
611
+ providerSessionId: sessionBinding.providerSessionId,
395
612
  workspace: resolvedDir,
396
613
  currentModel: initialModel,
397
614
  sessionId: key,
@@ -399,6 +616,10 @@ export class DaemonCliManager {
399
616
  });
400
617
 
401
618
  this.deps.onStatusChange();
619
+ return {
620
+ runtimeSessionId: key,
621
+ providerSessionId: sessionBinding.providerSessionId,
622
+ };
402
623
  }
403
624
 
404
625
  async stopSession(key: string): Promise<void> {
@@ -464,6 +685,12 @@ export class DaemonCliManager {
464
685
  if (!providerMeta || providerMeta.category !== 'cli') continue;
465
686
 
466
687
  const resolvedProvider = this.providerLoader.resolve(normalizedType) || providerMeta;
688
+ const sessionBinding = resolveCliSessionBinding(
689
+ resolvedProvider,
690
+ normalizedType,
691
+ record.cliArgs,
692
+ record.providerSessionId,
693
+ );
467
694
  try {
468
695
  await this.registerCliInstance(
469
696
  record.runtimeId,
@@ -474,6 +701,10 @@ export class DaemonCliManager {
474
701
  resolvedProvider,
475
702
  {},
476
703
  true,
704
+ {
705
+ providerSessionId: sessionBinding.providerSessionId,
706
+ launchMode: 'manual',
707
+ },
477
708
  );
478
709
  restored += 1;
479
710
  LOG.info('CLI', `♻ Restored hosted runtime: ${record.runtimeKey || record.runtimeId} (${record.displayName || record.workspace})`);
@@ -562,17 +793,23 @@ export class DaemonCliManager {
562
793
  const launchSource = resolved.source;
563
794
  if (!cliType) throw new Error('cliType required');
564
795
 
565
- await this.startSession(cliType, dir, args?.cliArgs, args?.initialModel);
566
-
567
- // On startSession success, new UUID key exists in adapters (last added item)
568
- let newKey: string | null = null;
569
- for (const [k, adapter] of this.adapters) {
570
- if (adapter.cliType === cliType && adapter.workingDir === dir) {
571
- newKey = k; // Last match = just added item
572
- }
573
- }
796
+ const started = await this.startSession(
797
+ cliType,
798
+ dir,
799
+ args?.cliArgs,
800
+ args?.initialModel,
801
+ { resumeSessionId: args?.resumeSessionId },
802
+ );
574
803
 
575
- return { success: true, cliType, dir, id: newKey, launchSource };
804
+ return {
805
+ success: true,
806
+ cliType,
807
+ dir,
808
+ id: started.runtimeSessionId,
809
+ sessionId: started.runtimeSessionId,
810
+ providerSessionId: started.providerSessionId,
811
+ launchSource,
812
+ };
576
813
  }
577
814
  case 'stop_cli': {
578
815
  const cliType = args?.cliType;