@adhdev/daemon-core 0.8.52 → 0.8.54

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/commands/cli-manager.d.ts +8 -1
  2. package/dist/index.d.ts +5 -1
  3. package/dist/index.js +896 -299
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +887 -300
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/providers/acp-provider-instance.d.ts +2 -1
  8. package/dist/providers/cli-provider-instance.d.ts +3 -1
  9. package/dist/providers/cli-script-results.d.ts +8 -0
  10. package/dist/providers/contracts.d.ts +58 -9
  11. package/dist/providers/control-effects.d.ts +4 -1
  12. package/dist/providers/io-contracts.d.ts +91 -0
  13. package/dist/providers/provider-schema.d.ts +5 -0
  14. package/dist/session-host/runtime-surface.d.ts +16 -0
  15. package/dist/session-host/startup-restore-policy.d.ts +1 -0
  16. package/dist/shared-types.d.ts +6 -0
  17. package/dist/types.d.ts +3 -3
  18. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +16 -1
  19. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +16 -1
  20. package/node_modules/@adhdev/session-host-core/dist/index.js +59 -0
  21. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  22. package/node_modules/@adhdev/session-host-core/dist/index.mjs +54 -0
  23. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  24. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  25. package/package.json +1 -1
  26. package/src/commands/cdp-commands.ts +27 -0
  27. package/src/commands/chat-commands.ts +7 -2
  28. package/src/commands/cli-manager.ts +9 -5
  29. package/src/commands/handler.ts +1 -9
  30. package/src/commands/stream-commands.ts +26 -36
  31. package/src/daemon/dev-server.ts +4 -18
  32. package/src/index.d.ts +3 -0
  33. package/src/index.ts +12 -1
  34. package/src/providers/acp-provider-instance.ts +156 -14
  35. package/src/providers/cli-provider-instance.ts +54 -5
  36. package/src/providers/cli-script-results.ts +39 -0
  37. package/src/providers/contracts.ts +72 -19
  38. package/src/providers/control-effects.ts +86 -1
  39. package/src/providers/io-contracts.ts +340 -0
  40. package/src/providers/provider-loader.ts +18 -13
  41. package/src/providers/provider-schema.ts +154 -0
  42. package/src/session-host/runtime-surface.ts +80 -0
  43. package/src/session-host/startup-restore-policy.d.ts +1 -0
  44. package/src/session-host/startup-restore-policy.js +7 -0
  45. package/src/session-host/startup-restore-policy.ts +7 -0
  46. package/src/shared-types.ts +6 -0
  47. package/src/status/builders.ts +5 -81
  48. package/src/types.ts +3 -3
@@ -416,10 +416,6 @@ export class DaemonCommandHandler implements CommandHelpers {
416
416
  'pty_input',
417
417
  'pty_resize',
418
418
  'invoke_provider_script',
419
- 'list_extension_models',
420
- 'set_extension_model',
421
- 'list_extension_modes',
422
- 'set_extension_mode',
423
419
  ]);
424
420
 
425
421
  if (this._currentRoute.sessionLookupFailed && sessionScopedCommands.has(cmd)) {
@@ -511,12 +507,8 @@ export class DaemonCommandHandler implements CommandHelpers {
511
507
  case 'get_ide_extensions': return Stream.handleGetIdeExtensions(this, args);
512
508
  case 'set_ide_extension': return Stream.handleSetIdeExtension(this, args);
513
509
 
514
- // ─── Extension Model / Mode Control (stream-commands.ts) ──────────
510
+ // ─── Provider control execution (stream-commands.ts) ──────────
515
511
  case 'invoke_provider_script': return Stream.handleProviderScript(this, args);
516
- case 'list_extension_models': return Stream.handleExtensionScript(this, args, 'listModels');
517
- case 'set_extension_model': return Stream.handleExtensionScript(this, args, 'setModel');
518
- case 'list_extension_modes': return Stream.handleExtensionScript(this, args, 'listModes');
519
- case 'set_extension_mode': return Stream.handleExtensionScript(this, args, 'setMode');
520
512
 
521
513
  // ─── Provider Auto-Fix / Clone (DevServer proxy) ──────────
522
514
  case 'provider_auto_fix': return this.proxyDevServerPost(args, 'auto-implement');
@@ -6,6 +6,12 @@
6
6
  import type { CommandResult, CommandHelpers } from './handler.js';
7
7
  import type { ProviderLoader } from '../providers/provider-loader.js';
8
8
  import type { ProviderInstance } from '../providers/provider-instance.js';
9
+ import { getCliScriptCommand, parseCliScriptResult } from '../providers/cli-script-results.js';
10
+ import {
11
+ normalizeControlInvokeResult,
12
+ normalizeControlListResult,
13
+ normalizeControlSetResult,
14
+ } from '../providers/control-effects.js';
9
15
  import { LOG } from '../logging/logger.js';
10
16
 
11
17
  interface CliPresentationInstance extends ProviderInstance {
@@ -113,42 +119,22 @@ function normalizeProviderScriptArgs(args: any): Record<string, any> {
113
119
  return normalizedArgs;
114
120
  }
115
121
 
116
- function parseScriptResult(result: unknown): { success: boolean; payload: any } {
117
- if (typeof result === 'string') {
118
- try {
119
- const parsed = JSON.parse(result);
120
- if (parsed && typeof parsed === 'object' && parsed.success === false) {
121
- return { success: false, payload: parsed };
122
- }
123
- return { success: true, payload: parsed };
124
- } catch {
125
- return { success: true, payload: { result } };
126
- }
127
- }
128
- if (result && typeof result === 'object' && 'success' in result && result.success === false) {
129
- return { success: false, payload: result };
122
+ function buildControlScriptResult(scriptName: string, payload: any): Record<string, unknown> {
123
+ if (!payload || typeof payload !== 'object') return {};
124
+ if (Array.isArray(payload.options) || Array.isArray(payload.models) || Array.isArray(payload.modes)) {
125
+ return { controlResult: normalizeControlListResult(payload) };
130
126
  }
131
- return { success: true, payload: result };
132
- }
133
-
134
- function getCliScriptCommand(payload: any): { type: string; text?: string } | null {
135
- if (!payload || typeof payload !== 'object') return null;
136
127
 
137
- if (typeof payload.sendMessage === 'string' && payload.sendMessage.trim()) {
138
- return { type: 'send_message', text: payload.sendMessage.trim() };
128
+ const looksLikeValueMutation = /^set|^change/i.test(scriptName)
129
+ || payload.currentValue !== undefined
130
+ || payload.value !== undefined;
131
+ if (looksLikeValueMutation) {
132
+ return { controlResult: normalizeControlSetResult(payload) };
139
133
  }
140
-
141
- const command = payload.command;
142
- if (!command || typeof command !== 'object') return null;
143
- if (command.type !== 'send_message' && command.type !== 'pty_write') return null;
144
-
145
- const text = typeof command.text === 'string'
146
- ? command.text.trim()
147
- : typeof command.message === 'string'
148
- ? command.message.trim()
149
- : '';
150
- if (!text) return null;
151
- return { type: command.type, text };
134
+ if (payload.ok !== undefined || payload.success !== undefined || Array.isArray(payload.effects)) {
135
+ return { controlResult: normalizeControlInvokeResult(payload) };
136
+ }
137
+ return {};
152
138
  }
153
139
 
154
140
  function applyProviderPatch(h: CommandHelpers, args: any, payload: any): void {
@@ -195,7 +181,7 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
195
181
  }
196
182
  try {
197
183
  const raw = await adapter.invokeScript(actualScriptName, normalizedArgs);
198
- const parsed = parseScriptResult(raw);
184
+ const parsed = parseCliScriptResult(raw);
199
185
  if (!parsed.success) {
200
186
  return { success: false, ...(parsed.payload || {}) };
201
187
  }
@@ -206,7 +192,11 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
206
192
  adapter.writeRaw(cliCommand.text + '\r');
207
193
  }
208
194
  applyProviderPatch(h, args, parsed.payload);
209
- return { success: true, ...(parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : { result: parsed.payload }) };
195
+ return {
196
+ success: true,
197
+ ...(parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : { result: parsed.payload }),
198
+ ...buildControlScriptResult(scriptName, parsed.payload),
199
+ };
210
200
  } catch (e: any) {
211
201
  return { success: false, error: `Script execution failed: ${e.message}` };
212
202
  }
@@ -282,7 +272,7 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
282
272
  if (parsed && typeof parsed === 'object' && parsed.success === false) {
283
273
  return { success: false, ...parsed };
284
274
  }
285
- return { success: true, ...parsed };
275
+ return { success: true, ...parsed, ...buildControlScriptResult(scriptName, parsed) };
286
276
  } catch {
287
277
  return { success: true, result };
288
278
  }
@@ -21,6 +21,7 @@ import * as path from 'path';
21
21
  import * as os from 'os';
22
22
  import type { ProviderLoader } from '../providers/provider-loader.js';
23
23
  import type { ProviderCategory, ProviderModule, ProviderScripts, ProviderSettingDef } from '../providers/contracts.js';
24
+ import { validateProviderDefinition } from '../providers/provider-schema.js';
24
25
  import type { ChildProcess } from 'child_process';
25
26
  import type { DaemonCdpManager } from '../cdp/manager.js';
26
27
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
@@ -773,24 +774,9 @@ export class DevServer implements DevServerContext {
773
774
  const warnings: string[] = [];
774
775
  try {
775
776
  const config = typeof content === 'string' ? JSON.parse(content) : content;
776
- // Required fields
777
- if (!config.type) errors.push('Missing required field: type');
778
- if (!config.name) errors.push('Missing required field: name');
779
- if (!config.category) errors.push('Missing required field: category');
780
- else if (!['ide', 'extension', 'cli', 'acp'].includes(config.category)) errors.push(`Invalid category: ${config.category}`);
781
- // Category-specific
782
- if (config.category === 'ide' || config.category === 'extension') {
783
- if (!config.cdpPorts || !Array.isArray(config.cdpPorts) || config.cdpPorts.length === 0)
784
- warnings.push('IDE/Extension providers should have cdpPorts');
785
- if (config.category === 'extension' && !config.extensionId)
786
- warnings.push('Extension providers should have extensionId');
787
- }
788
- if (config.category === 'acp' || config.category === 'cli') {
789
- if (!config.spawn) errors.push('ACP/CLI providers must have spawn config');
790
- else {
791
- if (!config.spawn.command) errors.push('spawn.command is required');
792
- }
793
- }
777
+ const validation = validateProviderDefinition(config);
778
+ errors.push(...validation.errors);
779
+ warnings.push(...validation.warnings);
794
780
  // Settings validation
795
781
  if (config.settings) {
796
782
  for (const [key, val] of Object.entries(config.settings)) {
package/src/index.d.ts CHANGED
@@ -76,6 +76,9 @@ export type { PtyRuntimeTransport, PtyTransportFactory, PtySpawnOptions } from '
76
76
  export { SessionHostPtyTransportFactory } from './cli-adapters/session-host-transport.js';
77
77
  export type { HostedCliRuntimeDescriptor, CliTransportFactoryParams } from './commands/cli-manager.js';
78
78
  export { ensureSessionHostReady, listHostedCliRuntimes } from './session-host/runtime-support.js';
79
+ export { getSessionHostRecoveryLabel, getSessionHostSurfaceKind, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, } from './session-host/runtime-surface.js';
80
+ export type { SessionHostSurfaceKind, SessionHostSurfaceRecordLike } from './session-host/runtime-surface.js';
81
+ export { shouldAutoRestoreHostedSessionsOnStartup } from './session-host/startup-restore-policy.js';
79
82
  export type { SessionHostEndpoint } from '@adhdev/session-host-core';
80
83
  export { getAIExtensions, installExtensions, launchIDE, isExtensionInstalled } from './installer.js';
81
84
  export type { ExtensionInfo as InstallerExtensionInfo } from './installer.js';
package/src/index.ts CHANGED
@@ -194,7 +194,8 @@ export { ProviderInstanceManager } from './providers/provider-instance-manager.j
194
194
  export { IdeProviderInstance } from './providers/ide-provider-instance.js';
195
195
  export { CliProviderInstance } from './providers/cli-provider-instance.js';
196
196
  export { AcpProviderInstance } from './providers/acp-provider-instance.js';
197
- export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability } from './providers/contracts.js';
197
+ export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability, InputEnvelope, InputPart, MessagePart, ControlListResult, ControlSetResult, ControlInvokeResult } from './providers/contracts.js';
198
+ export { normalizeInputEnvelope, normalizeMessageParts, flattenMessageParts } from './providers/io-contracts.js';
198
199
  export { VersionArchive, detectAllVersions } from './providers/version-archive.js';
199
200
  export type { ProviderVersionInfo, VersionHistory } from './providers/version-archive.js';
200
201
 
@@ -214,6 +215,16 @@ export {
214
215
  resolveSessionHostAppName,
215
216
  } from './session-host/app-name.js';
216
217
  export { ensureSessionHostReady, listHostedCliRuntimes } from './session-host/runtime-support.js';
218
+ export {
219
+ getSessionHostRecoveryLabel,
220
+ getSessionHostSurfaceKind,
221
+ isSessionHostLiveRuntime,
222
+ isSessionHostRecoverySnapshot,
223
+ partitionSessionHostDiagnosticsSessions,
224
+ partitionSessionHostRecords,
225
+ } from './session-host/runtime-surface.js';
226
+ export type { SessionHostSurfaceKind, SessionHostSurfaceRecordLike } from './session-host/runtime-surface.js';
227
+ export { shouldAutoRestoreHostedSessionsOnStartup } from './session-host/startup-restore-policy.js';
217
228
  export type { SessionHostEndpoint } from '@adhdev/session-host-core';
218
229
 
219
230
  // ── Installer ──
@@ -15,6 +15,7 @@
15
15
  * 5. dispose() → kill process
16
16
  */
17
17
 
18
+ import * as path from 'path';
18
19
  import { Readable, Writable } from 'stream';
19
20
  import { spawn, type ChildProcess } from 'child_process';
20
21
  import {
@@ -45,8 +46,8 @@ import {
45
46
  type ToolCallStatus,
46
47
  type SessionConfigOption,
47
48
  } from '@agentclientprotocol/sdk';
48
- import type { ProviderModule, ContentBlock, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
49
- import { normalizeContent, flattenContent } from './contracts.js';
49
+ import type { ProviderModule, ContentBlock, InputEnvelope, InputPart, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
50
+ import { normalizeContent, flattenContent, normalizeInputEnvelope } from './contracts.js';
50
51
  import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext } from './provider-instance.js';
51
52
  import { StatusMonitor } from './status-monitor.js';
52
53
  import { LOG } from '../logging/logger.js';
@@ -83,6 +84,121 @@ interface AcpMode {
83
84
  description?: string;
84
85
  }
85
86
 
87
+ interface PromptCapabilityFlags {
88
+ image: boolean;
89
+ audio: boolean;
90
+ embeddedContext: boolean;
91
+ }
92
+
93
+ function getPromptCapabilityFlags(agentCapabilities?: Record<string, any>): PromptCapabilityFlags {
94
+ const prompt = agentCapabilities?.promptCapabilities || {};
95
+ return {
96
+ image: prompt.image === true,
97
+ audio: prompt.audio === true,
98
+ embeddedContext: prompt.embeddedContext === true,
99
+ };
100
+ }
101
+
102
+ function getResourceNameFromUri(uri: string, fallback: string): string {
103
+ try {
104
+ if (uri.startsWith('file://')) {
105
+ return path.basename(new URL(uri).pathname) || fallback;
106
+ }
107
+ return path.basename(uri) || fallback;
108
+ } catch {
109
+ return fallback;
110
+ }
111
+ }
112
+
113
+ function inputPartToResourceLink(part: Extract<InputPart, { type: 'image' | 'audio' | 'video' | 'resource' }>, fallbackName: string): ContentBlock | null {
114
+ if (!part.uri) return null;
115
+ return {
116
+ type: 'resource_link',
117
+ uri: part.uri,
118
+ name: getResourceNameFromUri(part.uri, fallbackName),
119
+ ...(part.mimeType ? { mimeType: part.mimeType } : {}),
120
+ };
121
+ }
122
+
123
+ function appendPromptText(promptParts: ContentBlock[], text: string | undefined): void {
124
+ const normalized = typeof text === 'string' ? text.trim() : '';
125
+ if (!normalized) return;
126
+ const last = promptParts[promptParts.length - 1];
127
+ if (last?.type === 'text' && last.text === normalized) return;
128
+ promptParts.push({ type: 'text', text: normalized });
129
+ }
130
+
131
+ export function buildAcpPromptParts(input: InputEnvelope, agentCapabilities?: Record<string, any>): ContentBlock[] {
132
+ const caps = getPromptCapabilityFlags(agentCapabilities);
133
+ const promptParts: ContentBlock[] = [];
134
+
135
+ for (const part of input.parts) {
136
+ if (part.type === 'text') {
137
+ promptParts.push({ type: 'text', text: part.text });
138
+ continue;
139
+ }
140
+
141
+ if (part.type === 'image') {
142
+ if (caps.image && part.data) {
143
+ promptParts.push({
144
+ type: 'image',
145
+ data: part.data,
146
+ mimeType: part.mimeType,
147
+ ...(part.uri ? { uri: part.uri } : {}),
148
+ });
149
+ continue;
150
+ }
151
+ const fallback = inputPartToResourceLink(part, 'image');
152
+ if (fallback) promptParts.push(fallback);
153
+ appendPromptText(promptParts, part.alt || (!part.uri ? `Attached image (${part.mimeType})` : undefined));
154
+ continue;
155
+ }
156
+
157
+ if (part.type === 'audio') {
158
+ if (caps.audio && part.data) {
159
+ promptParts.push({
160
+ type: 'audio',
161
+ data: part.data,
162
+ mimeType: part.mimeType,
163
+ });
164
+ continue;
165
+ }
166
+ const fallback = inputPartToResourceLink(part, 'audio');
167
+ if (fallback) promptParts.push(fallback);
168
+ appendPromptText(promptParts, part.transcript || (!part.uri ? `Attached audio (${part.mimeType})` : undefined));
169
+ continue;
170
+ }
171
+
172
+ if (part.type === 'resource') {
173
+ if (caps.embeddedContext && (part.text || part.data)) {
174
+ promptParts.push({
175
+ type: 'resource',
176
+ resource: part.text
177
+ ? { uri: part.uri, text: part.text, mimeType: part.mimeType ?? null }
178
+ : { uri: part.uri, blob: part.data || '', mimeType: part.mimeType ?? null },
179
+ });
180
+ continue;
181
+ }
182
+ const fallback = inputPartToResourceLink(part, part.name || 'resource');
183
+ if (fallback) promptParts.push(fallback);
184
+ appendPromptText(promptParts, part.text || (!part.uri && part.name ? part.name : undefined));
185
+ continue;
186
+ }
187
+
188
+ if (part.type === 'video') {
189
+ const fallback = inputPartToResourceLink(part, 'video');
190
+ if (fallback) promptParts.push(fallback);
191
+ appendPromptText(promptParts, !part.uri ? `Attached video (${part.mimeType})` : undefined);
192
+ }
193
+ }
194
+
195
+ if (!promptParts.some((part) => part.type === 'text') && input.textFallback) {
196
+ promptParts.unshift({ type: 'text', text: input.textFallback });
197
+ }
198
+
199
+ return promptParts;
200
+ }
201
+
86
202
  // ─── AcpProviderInstance ───────────────────────────
87
203
 
88
204
  export class AcpProviderInstance implements ProviderInstance {
@@ -237,8 +353,10 @@ export class AcpProviderInstance implements ProviderInstance {
237
353
  }
238
354
 
239
355
  onEvent(event: string, data?: any): void {
240
- if (event === 'send_message' && data?.text) {
241
- this.sendPrompt(data.text).catch(e =>
356
+ if (event === 'send_message') {
357
+ const input = normalizeInputEnvelope(data)
358
+ const promptParts = buildAcpPromptParts(input, this.agentCapabilities)
359
+ this.sendPrompt(input.textFallback, promptParts.length > 0 ? promptParts : undefined).catch(e =>
242
360
  this.log.warn(`[${this.type}] sendPrompt error: ${e?.message}`)
243
361
  );
244
362
  } else if (event === 'resolve_action') {
@@ -768,19 +886,36 @@ export class AcpProviderInstance implements ProviderInstance {
768
886
  }
769
887
 
770
888
  // Build prompt content
771
- let promptParts: any[];
772
- if (contentBlocks && contentBlocks.length > 0) {
773
- // Rich content — forward ContentBlock[] as ACP prompt parts
774
- promptParts = contentBlocks.map(b => {
889
+ const promptParts: any[] = contentBlocks && contentBlocks.length > 0
890
+ ? contentBlocks.map((b) => {
775
891
  if (b.type === 'text') return { type: 'text', text: b.text };
776
- if (b.type === 'image') return { type: 'image', data: b.data, mimeType: b.mimeType };
777
- if (b.type === 'resource_link') return { type: 'resource_link', uri: b.uri, name: b.name };
892
+ if (b.type === 'image') {
893
+ return {
894
+ type: 'image',
895
+ data: b.data,
896
+ mimeType: b.mimeType,
897
+ ...(b.uri ? { uri: b.uri } : {}),
898
+ };
899
+ }
900
+ if (b.type === 'audio') {
901
+ return {
902
+ type: 'audio',
903
+ data: b.data,
904
+ mimeType: b.mimeType,
905
+ };
906
+ }
907
+ if (b.type === 'resource_link') {
908
+ return {
909
+ type: 'resource_link',
910
+ uri: b.uri,
911
+ name: b.name,
912
+ ...(b.mimeType ? { mimeType: b.mimeType } : {}),
913
+ };
914
+ }
778
915
  if (b.type === 'resource') return { type: 'resource', resource: b.resource };
779
916
  return { type: 'text', text: flattenContent([b]) };
780
- });
781
- } else {
782
- promptParts = [{ type: 'text', text }];
783
- }
917
+ })
918
+ : [{ type: 'text', text }];
784
919
 
785
920
  // Add user message locally (store as ContentBlock[])
786
921
  this.messages.push({
@@ -862,6 +997,13 @@ export class AcpProviderInstance implements ProviderInstance {
862
997
  type: 'image',
863
998
  data: content.data,
864
999
  mimeType: content.mimeType,
1000
+ ...(content.uri ? { uri: content.uri } : {}),
1001
+ });
1002
+ } else if (content.type === 'audio') {
1003
+ this.partialBlocks.push({
1004
+ type: 'audio',
1005
+ data: content.data,
1006
+ mimeType: content.mimeType,
865
1007
  });
866
1008
  } else if (content.type === 'resource_link') {
867
1009
  this.partialBlocks.push({
@@ -10,7 +10,7 @@ import * as path from 'path';
10
10
  import * as crypto from 'crypto';
11
11
  import * as fs from 'fs';
12
12
  import { createRequire } from 'node:module';
13
- import type { ProviderModule } from './contracts.js';
13
+ import { normalizeInputEnvelope, 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';
@@ -21,6 +21,7 @@ import { LOG } from '../logging/logger.js';
21
21
  import type { ChatMessage } from '../types.js';
22
22
  import { extractProviderControlValues, normalizeProviderEffects } from './control-effects.js';
23
23
  import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
24
+ import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
24
25
 
25
26
  let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
26
27
  prepare(sql: string): { get(...params: Array<string | number>): unknown };
@@ -42,6 +43,29 @@ function getDatabaseSync() {
42
43
  return CachedDatabaseSync;
43
44
  }
44
45
 
46
+ export function getForcedNewSessionScriptName(
47
+ provider: ProviderModule | undefined,
48
+ launchMode: 'new' | 'resume' | 'manual',
49
+ ): string | null {
50
+ if (!provider || launchMode !== 'new') return null;
51
+ const resume = provider.resume;
52
+ if (!resume?.supported) return null;
53
+ if (Array.isArray(resume.newSessionArgs) && resume.newSessionArgs.length > 0) return null;
54
+
55
+ const controls = Array.isArray((provider as any).controls) ? (provider as any).controls : [];
56
+ for (const control of controls) {
57
+ if (control?.type !== 'action') continue;
58
+ const invokeScript = typeof control?.invokeScript === 'string' ? control.invokeScript.trim() : '';
59
+ if (!invokeScript) continue;
60
+ const controlId = typeof control?.id === 'string' ? control.id.trim() : '';
61
+ if (controlId === 'new_session' || /^new.?session$/i.test(invokeScript)) {
62
+ return invokeScript;
63
+ }
64
+ }
65
+
66
+ return null;
67
+ }
68
+
45
69
  export class CliProviderInstance implements ProviderInstance {
46
70
  readonly type: string;
47
71
  readonly category = 'cli' as const;
@@ -135,6 +159,7 @@ export class CliProviderInstance implements ProviderInstance {
135
159
 
136
160
  // PTY spawn
137
161
  await this.adapter.spawn();
162
+ await this.enforceFreshSessionLaunchIfNeeded();
138
163
  this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
139
164
  if (this.providerSessionId) {
140
165
  this.historyWriter.compactHistorySession(this.type, this.providerSessionId);
@@ -370,10 +395,13 @@ export class CliProviderInstance implements ProviderInstance {
370
395
  }
371
396
 
372
397
  onEvent(event: string, data?: any): void {
373
- if (event === 'send_message' && data?.text) {
374
- void this.adapter.sendMessage(data.text).catch((e: any) => {
375
- LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
376
- });
398
+ if (event === 'send_message') {
399
+ const input = normalizeInputEnvelope(data);
400
+ if (input.textFallback) {
401
+ void this.adapter.sendMessage(input.textFallback).catch((e: any) => {
402
+ LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
403
+ });
404
+ }
377
405
  } else if (event === 'server_connected' && data?.serverConn) {
378
406
  this.adapter.setServerConn(data.serverConn);
379
407
  } else if (event === 'resolve_action' && data) {
@@ -394,6 +422,27 @@ export class CliProviderInstance implements ProviderInstance {
394
422
  private completedDebounceTimer: NodeJS.Timeout | null = null;
395
423
  private completedDebouncePending: { chatTitle: string; duration: number; timestamp: number } | null = null;
396
424
 
425
+ private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
426
+ const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
427
+ if (!scriptName) return;
428
+
429
+ LOG.info('CLI', `[${this.type}] forcing fresh session launch via script: ${scriptName}`);
430
+ const raw = await this.adapter.invokeScript(scriptName, {});
431
+ const parsed = parseCliScriptResult(raw);
432
+ if (!parsed.success) {
433
+ throw new Error(parsed.payload?.error || `Failed to invoke fresh-session script '${scriptName}'`);
434
+ }
435
+
436
+ const cliCommand = getCliScriptCommand(parsed.payload);
437
+ if (cliCommand?.type === 'send_message' && cliCommand.text) {
438
+ await this.adapter.sendMessage(cliCommand.text);
439
+ } else if (cliCommand?.type === 'pty_write' && cliCommand.text) {
440
+ this.adapter.writeRaw(cliCommand.text + '\r');
441
+ }
442
+
443
+ this.applyProviderResponse(parsed.payload, { phase: 'immediate' });
444
+ }
445
+
397
446
  private detectStatusTransition(): void {
398
447
  const now = Date.now();
399
448
  const adapterStatus = this.adapter.getStatus();
@@ -0,0 +1,39 @@
1
+ export function parseCliScriptResult(result: unknown): { success: boolean; payload: any } {
2
+ if (typeof result === 'string') {
3
+ try {
4
+ const parsed = JSON.parse(result)
5
+ if (parsed && typeof parsed === 'object' && parsed.success === false) {
6
+ return { success: false, payload: parsed }
7
+ }
8
+ return { success: true, payload: parsed }
9
+ } catch {
10
+ return { success: true, payload: { result } }
11
+ }
12
+ }
13
+
14
+ if (result && typeof result === 'object' && 'success' in result && result.success === false) {
15
+ return { success: false, payload: result }
16
+ }
17
+
18
+ return { success: true, payload: result }
19
+ }
20
+
21
+ export function getCliScriptCommand(payload: any): { type: string; text?: string } | null {
22
+ if (!payload || typeof payload !== 'object') return null
23
+
24
+ if (typeof payload.sendMessage === 'string' && payload.sendMessage.trim()) {
25
+ return { type: 'send_message', text: payload.sendMessage.trim() }
26
+ }
27
+
28
+ const command = payload.command
29
+ if (!command || typeof command !== 'object') return null
30
+ if (command.type !== 'send_message' && command.type !== 'pty_write') return null
31
+
32
+ const text = typeof command.text === 'string'
33
+ ? command.text.trim()
34
+ : typeof command.message === 'string'
35
+ ? command.message.trim()
36
+ : ''
37
+ if (!text) return null
38
+ return { type: command.type, text }
39
+ }