@adhdev/daemon-core 0.7.44 → 0.7.46

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 (48) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +1 -0
  2. package/dist/cli-adapters/pty-transport.d.ts +1 -0
  3. package/dist/commands/cli-manager.d.ts +11 -2
  4. package/dist/config/chat-history.d.ts +29 -2
  5. package/dist/config/config.d.ts +4 -0
  6. package/dist/config/recent-activity.d.ts +3 -1
  7. package/dist/config/saved-sessions.d.ts +22 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +4623 -3973
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +4617 -3969
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/providers/cli-provider-instance.d.ts +24 -1
  14. package/dist/providers/contracts.d.ts +3 -0
  15. package/dist/providers/provider-instance.d.ts +1 -0
  16. package/dist/shared-types.d.ts +2 -0
  17. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
  18. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
  19. package/node_modules/@adhdev/session-host-core/dist/index.js +11 -2
  20. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  21. package/node_modules/@adhdev/session-host-core/dist/index.mjs +11 -2
  22. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  23. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  24. package/package.json +1 -1
  25. package/src/boot/daemon-lifecycle.ts +19 -15
  26. package/src/cli-adapters/provider-cli-adapter.ts +11 -5
  27. package/src/cli-adapters/pty-transport.ts +1 -0
  28. package/src/cli-adapters/session-host-transport.ts +19 -0
  29. package/src/commands/chat-commands.ts +28 -8
  30. package/src/commands/cli-manager.ts +259 -22
  31. package/src/commands/router.ts +52 -1
  32. package/src/config/chat-history.ts +193 -10
  33. package/src/config/config.d.ts +4 -0
  34. package/src/config/config.ts +6 -0
  35. package/src/config/recent-activity.ts +13 -2
  36. package/src/config/saved-sessions.ts +73 -0
  37. package/src/daemon/dev-auto-implement.ts +23 -5
  38. package/src/daemon/dev-server.ts +22 -4
  39. package/src/index.ts +2 -0
  40. package/src/providers/cli-provider-instance.ts +205 -4
  41. package/src/providers/contracts.ts +3 -0
  42. package/src/providers/provider-instance.d.ts +1 -0
  43. package/src/providers/provider-instance.ts +1 -0
  44. package/src/session-host/runtime-support.ts +1 -0
  45. package/src/shared-types.d.ts +2 -0
  46. package/src/shared-types.ts +2 -0
  47. package/src/status/builders.ts +1 -0
  48. package/src/status/snapshot.ts +1 -0
@@ -276,7 +276,7 @@ function shSingleQuote(arg: string): string {
276
276
  return `'${arg.replace(/'/g, `'\\''`)}'`;
277
277
  }
278
278
 
279
- function estimatePromptDisplayLines(text: string, cols = 100): number {
279
+ function estimatePromptDisplayLines(text: string, cols = 80): number {
280
280
  const normalized = String(text || '').replace(/\r/g, '');
281
281
  if (!normalized) return 1;
282
282
  return normalized
@@ -433,7 +433,7 @@ export class ProviderCliAdapter implements CliAdapter {
433
433
  /** Full accumulated raw PTY output (with ANSI) */
434
434
  private accumulatedRawBuffer: string = '';
435
435
  /** Current visible terminal screen snapshot */
436
- private terminalScreen = new TerminalScreen(30, 100);
436
+ private terminalScreen = new TerminalScreen(24, 80);
437
437
  /** Max accumulated buffer size (last 50KB) */
438
438
  private static readonly MAX_ACCUMULATED_BUFFER = 50000;
439
439
  private currentTurnScope: TurnParseScope | null = null;
@@ -620,8 +620,8 @@ export class ProviderCliAdapter implements CliAdapter {
620
620
  }
621
621
 
622
622
  const ptyOpts = {
623
- cols: 100,
624
- rows: 30,
623
+ cols: 80,
624
+ rows: 24,
625
625
  cwd: this.workingDir,
626
626
  env: buildCliSpawnEnv(process.env, spawnConfig.env),
627
627
  };
@@ -684,7 +684,7 @@ export class ProviderCliAdapter implements CliAdapter {
684
684
  this.spawnAt = Date.now();
685
685
  this.startupParseGate = true;
686
686
  this.startupBuffer = '';
687
- this.terminalScreen.reset(30, 100);
687
+ this.terminalScreen.reset(24, 80);
688
688
  this.pendingTerminalQueryTail = '';
689
689
  this.currentTurnScope = null;
690
690
  this.ready = false;
@@ -1025,6 +1025,7 @@ export class ProviderCliAdapter implements CliAdapter {
1025
1025
  title: parsed.title || this.cliName,
1026
1026
  messages: parsed.messages,
1027
1027
  activeModal: parsed.activeModal ?? this.activeModal,
1028
+ providerSessionId: typeof parsed.providerSessionId === 'string' ? parsed.providerSessionId : undefined,
1028
1029
  };
1029
1030
  }
1030
1031
 
@@ -1222,6 +1223,11 @@ export class ProviderCliAdapter implements CliAdapter {
1222
1223
  return this.ptyProcess.getMetadata();
1223
1224
  }
1224
1225
 
1226
+ updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
1227
+ if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
1228
+ this.ptyProcess.updateMeta(meta, replace);
1229
+ }
1230
+
1225
1231
  cancel(): void { this.shutdown(); }
1226
1232
 
1227
1233
  async saveAndStop(): Promise<void> {
@@ -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;
@@ -171,6 +171,25 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
171
171
  });
172
172
  }
173
173
 
174
+ updateMeta(meta: Record<string, unknown>, replace = false): void {
175
+ this.enqueue(async () => {
176
+ const response = await this.client.request<SessionHostRecord>({
177
+ type: 'update_session_meta',
178
+ payload: {
179
+ sessionId: this.options.runtimeId,
180
+ meta,
181
+ replace,
182
+ },
183
+ });
184
+ if (!response?.success) {
185
+ throw new Error(response.error || `Failed to update runtime meta ${this.options.runtimeId}`);
186
+ }
187
+ if (response.result) {
188
+ this.updateMetadata(response.result);
189
+ }
190
+ });
191
+ }
192
+
174
193
  private async boot(): Promise<void> {
175
194
  await this.client.connect();
176
195
  this.unsubscribe = this.client.onEvent((event: SessionHostEvent) => this.handleEvent(event));
@@ -59,6 +59,19 @@ function buildRecentSendKey(h: CommandHelpers, args: any, provider: any, text: s
59
59
  return `${transport}:${target}:${text.trim()}`;
60
60
  }
61
61
 
62
+ function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
63
+ const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
64
+ if (explicit) return explicit;
65
+
66
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
67
+ if (!targetSessionId) return undefined;
68
+
69
+ const instance = h.ctx.instanceManager?.getInstance(targetSessionId) as any;
70
+ const state = instance?.getState?.();
71
+ const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
72
+ return providerSessionId || targetSessionId;
73
+ }
74
+
62
75
  function isRecentDuplicateSend(key: string): boolean {
63
76
  const now = Date.now();
64
77
  for (const [candidate, ts] of recentSendByTarget.entries()) {
@@ -72,11 +85,11 @@ function isRecentDuplicateSend(key: string): boolean {
72
85
 
73
86
  export async function handleChatHistory(h: CommandHelpers, args: any): Promise<CommandResult> {
74
87
  const { agentType, offset, limit } = args;
75
- const instanceId = args?.targetSessionId;
88
+ const historySessionId = getHistorySessionId(h, args);
76
89
  try {
77
90
  const provider = h.getProvider(agentType);
78
91
  const agentStr = provider?.type || agentType || getCurrentProviderType(h);
79
- const result = readChatHistory(agentStr, offset || 0, limit || 30, instanceId);
92
+ const result = readChatHistory(agentStr, offset || 0, limit || 30, historySessionId);
80
93
  return { success: true, ...result, agent: agentStr };
81
94
  } catch (e: any) {
82
95
  return { success: false, error: e.message };
@@ -86,6 +99,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
86
99
  export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
87
100
  const provider = h.getProvider(args?.agentType);
88
101
  const transport = getTargetTransport(h, provider);
102
+ const historySessionId = getHistorySessionId(h, args);
89
103
 
90
104
  const _log = (msg: string) => LOG.debug('Command', `[read_chat] ${msg}`);
91
105
 
@@ -120,7 +134,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
120
134
  provider?.type || 'unknown_extension',
121
135
  parsed.messages || [],
122
136
  parsed.title,
123
- args?.targetSessionId
137
+ args?.targetSessionId,
138
+ historySessionId,
124
139
  );
125
140
  return { success: true, ...parsed };
126
141
  }
@@ -142,7 +157,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
142
157
  stream.agentType,
143
158
  stream.messages || [],
144
159
  undefined,
145
- args?.targetSessionId
160
+ args?.targetSessionId,
161
+ historySessionId,
146
162
  );
147
163
  return { success: true, messages: stream.messages || [], status: stream.status, agentType: stream.agentType };
148
164
  }
@@ -169,11 +185,12 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
169
185
  if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
170
186
  if (parsed && typeof parsed === 'object') {
171
187
  _log(`Webview OK: ${parsed.messages?.length || 0} msgs`);
172
- h.historyWriter.appendNewMessages(
188
+ h.historyWriter.appendNewMessages(
173
189
  provider?.type || getCurrentProviderType(h, 'unknown_webview'),
174
190
  parsed.messages || [],
175
191
  parsed.title,
176
- args?.targetSessionId
192
+ args?.targetSessionId,
193
+ historySessionId,
177
194
  );
178
195
  return { success: true, ...parsed };
179
196
  }
@@ -197,7 +214,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
197
214
  provider?.type || getCurrentProviderType(h, 'unknown_ide'),
198
215
  parsed.messages || [],
199
216
  parsed.title,
200
- args?.targetSessionId
217
+ args?.targetSessionId,
218
+ historySessionId,
201
219
  );
202
220
  return { success: true, ...parsed };
203
221
  }
@@ -216,13 +234,15 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
216
234
  const provider = h.getProvider(args?.agentType);
217
235
  const transport = getTargetTransport(h, provider);
218
236
  const dedupeKey = buildRecentSendKey(h, args, provider, text);
237
+ const historySessionId = getHistorySessionId(h, args);
219
238
 
220
239
  const _logSendSuccess = (method: string, targetAgent?: string) => {
221
240
  h.historyWriter.appendNewMessages(
222
241
  targetAgent || provider?.type || getCurrentProviderType(h, 'unknown_agent'),
223
242
  [{ role: 'user', content: text, receivedAt: Date.now() }],
224
243
  undefined, // title
225
- args?.targetSessionId
244
+ args?.targetSessionId,
245
+ historySessionId,
226
246
  );
227
247
  return { success: true, sent: true, method, targetAgent };
228
248
  };
@@ -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;