@adhdev/daemon-core 0.8.28 → 0.8.30

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 (36) hide show
  1. package/dist/agent-stream/manager.d.ts +1 -1
  2. package/dist/agent-stream/provider-adapter.d.ts +5 -0
  3. package/dist/commands/router.d.ts +5 -0
  4. package/dist/config/chat-history.d.ts +12 -0
  5. package/dist/index.js +552 -52
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +552 -52
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/providers/acp-provider-instance.d.ts +1 -0
  10. package/dist/providers/approval-utils.d.ts +7 -0
  11. package/dist/providers/cli-provider-instance.d.ts +3 -0
  12. package/dist/providers/contracts.d.ts +2 -0
  13. package/dist/providers/ide-provider-instance.d.ts +1 -0
  14. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +24 -1
  15. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +24 -1
  16. package/node_modules/@adhdev/session-host-core/dist/index.js +6 -1
  17. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  18. package/node_modules/@adhdev/session-host-core/dist/index.mjs +6 -1
  19. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  20. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  21. package/package.json +1 -1
  22. package/src/agent-stream/manager.ts +2 -2
  23. package/src/agent-stream/poller.ts +38 -1
  24. package/src/agent-stream/provider-adapter.ts +97 -3
  25. package/src/cli-adapters/provider-cli-adapter.ts +3 -0
  26. package/src/commands/chat-commands.ts +53 -3
  27. package/src/commands/cli-manager.ts +14 -0
  28. package/src/commands/router.ts +11 -0
  29. package/src/config/chat-history.ts +269 -18
  30. package/src/providers/acp-provider-instance.ts +17 -2
  31. package/src/providers/approval-utils.ts +66 -0
  32. package/src/providers/cli-provider-instance.ts +47 -6
  33. package/src/providers/contracts.d.ts +1 -0
  34. package/src/providers/contracts.ts +3 -1
  35. package/src/providers/ide-provider-instance.ts +28 -23
  36. package/src/providers/provider-loader.ts +26 -2
@@ -244,7 +244,7 @@ export class DaemonAgentStreamManager {
244
244
  }
245
245
  }
246
246
 
247
- async resolveSessionAction(cdp: DaemonCdpManager, sessionId: string, action: 'approve' | 'reject'): Promise<boolean> {
247
+ async resolveSessionAction(cdp: DaemonCdpManager, sessionId: string, action: 'approve' | 'reject', button?: string): Promise<boolean> {
248
248
  await this.ensureSessionPanelOpen(sessionId);
249
249
  const target = this.getSessionTarget(sessionId);
250
250
  if (!target?.parentSessionId) return false;
@@ -255,7 +255,7 @@ export class DaemonAgentStreamManager {
255
255
  try {
256
256
  const evaluate: AgentEvaluateFn = (expr, timeout) =>
257
257
  cdp.evaluateInSessionFrame(agent.cdpSessionId, expr, timeout);
258
- return await agent.adapter.resolveAction(evaluate, action);
258
+ return await agent.adapter.resolveAction(evaluate, action, button);
259
259
  } catch (e) {
260
260
  this.logFn(`[AgentStream] resolveAction(${sessionId}) error: ${(e as Error).message}`);
261
261
  return false;
@@ -19,6 +19,7 @@ import type { SessionRegistry } from '../sessions/registry.js';
19
19
  import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
20
20
  import { LOG } from '../logging/logger.js';
21
21
  import type { AgentStreamState } from './types.js';
22
+ import { formatAutoApprovalMessage, pickApprovalButton } from '../providers/approval-utils.js';
22
23
 
23
24
  // ─── Types ───
24
25
 
@@ -189,7 +190,43 @@ export class AgentStreamPoller {
189
190
 
190
191
  try {
191
192
  await agentStreamManager.syncActiveSession(cdp, parentSessionId);
192
- const stream = await agentStreamManager.collectActiveSession(cdp, parentSessionId);
193
+ let stream = await agentStreamManager.collectActiveSession(cdp, parentSessionId);
194
+ if (stream?.status === 'waiting_approval') {
195
+ const autoApprove = providerLoader.getSettings(stream.agentType).autoApprove !== false;
196
+ if (autoApprove && resolvedActiveSessionId) {
197
+ const provider = providerLoader.getMeta(stream.agentType);
198
+ const { label: buttonLabel } = pickApprovalButton(stream.activeModal?.buttons, provider);
199
+ const approved = await agentStreamManager.resolveSessionAction(cdp, resolvedActiveSessionId, 'approve', buttonLabel);
200
+ if (approved) {
201
+ const effectId = [
202
+ 'auto_approval',
203
+ resolvedActiveSessionId,
204
+ String(stream.messages?.length || 0),
205
+ buttonLabel,
206
+ String(stream.activeModal?.message || '').trim(),
207
+ ].join(':');
208
+ stream = {
209
+ ...stream,
210
+ status: 'streaming',
211
+ activeModal: undefined,
212
+ effects: [
213
+ ...(stream.effects || []),
214
+ {
215
+ type: 'message',
216
+ id: effectId,
217
+ persist: true,
218
+ message: {
219
+ role: 'system',
220
+ senderName: 'System',
221
+ kind: 'system',
222
+ content: formatAutoApprovalMessage(stream.activeModal?.message, buttonLabel),
223
+ },
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ }
229
+ }
193
230
  this.deps.onStreamsUpdated?.(ideType, stream ? [stream] : []);
194
231
  } catch { }
195
232
  }
@@ -70,6 +70,64 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
70
70
  || /Cannot find context with specified id/i.test(reason);
71
71
  }
72
72
 
73
+ private titlesMatch(actual: string, expected: string): boolean {
74
+ const lhs = actual.trim().toLowerCase();
75
+ const rhs = expected.trim().toLowerCase();
76
+ if (!lhs || !rhs) return false;
77
+ return lhs === rhs || lhs.includes(rhs) || rhs.includes(lhs);
78
+ }
79
+
80
+ private messageCount(state: AgentStreamState | null | undefined): number {
81
+ return Array.isArray(state?.messages) ? state!.messages.length : 0;
82
+ }
83
+
84
+ private lastMessageSignature(state: AgentStreamState | null | undefined): string {
85
+ const messages = Array.isArray(state?.messages) ? state!.messages : [];
86
+ const last = messages[messages.length - 1] as any;
87
+ if (!last) return '';
88
+ return `${last.role || ''}:${String(last.content || '').replace(/\s+/g, ' ').trim()}`;
89
+ }
90
+
91
+ private async verifySendOutcome(
92
+ evaluate: AgentEvaluateFn,
93
+ before: AgentStreamState | null,
94
+ ): Promise<boolean> {
95
+ const beforeCount = this.messageCount(before);
96
+ const beforeSignature = this.lastMessageSignature(before);
97
+
98
+ for (let attempt = 0; attempt < 12; attempt += 1) {
99
+ await new Promise((resolve) => setTimeout(resolve, 250));
100
+ let state: AgentStreamState;
101
+ try {
102
+ state = await this.readChat(evaluate);
103
+ } catch {
104
+ continue;
105
+ }
106
+
107
+ if (state.status === 'waiting_approval') {
108
+ return true;
109
+ }
110
+
111
+ const afterCount = this.messageCount(state);
112
+ const afterSignature = this.lastMessageSignature(state);
113
+ if (afterCount > beforeCount) return true;
114
+ if (afterSignature && afterSignature !== beforeSignature) return true;
115
+ }
116
+
117
+ return false;
118
+ }
119
+
120
+ private async readStableBaselineState(evaluate: AgentEvaluateFn): Promise<AgentStreamState | null> {
121
+ const first = await this.readChat(evaluate);
122
+ if (this.messageCount(first) > 0 || this.lastMessageSignature(first)) {
123
+ return first;
124
+ }
125
+
126
+ await new Promise((resolve) => setTimeout(resolve, 150));
127
+ const second = await this.readChat(evaluate);
128
+ return this.messageCount(second) >= this.messageCount(first) ? second : first;
129
+ }
130
+
73
131
  async readChat(evaluate: AgentEvaluateFn): Promise<AgentStreamState> {
74
132
  const script = this.callScript('readChat');
75
133
  if (!script) return this.errorState('readChat script not available');
@@ -96,6 +154,9 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
96
154
  mode: data.mode,
97
155
  activeModal: data.activeModal,
98
156
  };
157
+ if (typeof data.title === 'string' && data.title.trim()) {
158
+ (state as any).title = data.title.trim();
159
+ }
99
160
  const controlValues = extractProviderControlValues(this.provider.controls, data);
100
161
  if (controlValues) state.controlValues = controlValues;
101
162
  const effects = normalizeProviderEffects(data);
@@ -120,6 +181,13 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
120
181
  }
121
182
 
122
183
  async sendMessage(evaluate: AgentEvaluateFn, text: string): Promise<void> {
184
+ let beforeState: AgentStreamState | null = null;
185
+ try {
186
+ beforeState = await this.readStableBaselineState(evaluate);
187
+ } catch {
188
+ beforeState = null;
189
+ }
190
+
123
191
  const params = { message: text };
124
192
  const script = this.callScript('sendMessage', params) || this.callScript('sendMessage', text);
125
193
  if (!script) throw new Error(`[${this.agentName}] sendMessage script not available`);
@@ -138,7 +206,9 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
138
206
  }
139
207
  if (parsed && typeof parsed === 'object') {
140
208
  if (parsed.sent === true || parsed.success === true || parsed.ok === true || parsed.submitted === true || parsed.dispatched === true) {
141
- return;
209
+ const verified = await this.verifySendOutcome(evaluate, beforeState);
210
+ if (verified) return;
211
+ throw new Error(`[${this.agentName}] sendMessage was not observed in chat state`);
142
212
  }
143
213
  if (typeof parsed.error === 'string' && parsed.error.trim()) {
144
214
  throw new Error(`[${this.agentName}] sendMessage failed: ${parsed.error}`);
@@ -151,7 +221,23 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
151
221
  async resolveAction(evaluate: AgentEvaluateFn, action: string, button?: string): Promise<boolean> {
152
222
  const script = this.callScript('resolveAction', { action, button });
153
223
  if (!script) return false; // Not supported if provider has no resolveAction
154
- return (await evaluate(script)) === true;
224
+ const result = await evaluate(script);
225
+ const parsed = this.parseMaybeJson(result);
226
+ if (parsed === true) return true;
227
+ if (typeof parsed === 'string') {
228
+ const normalized = parsed.trim().toLowerCase();
229
+ return normalized === 'ok'
230
+ || normalized === 'success'
231
+ || normalized === 'true'
232
+ || normalized === 'resolved'
233
+ || normalized === 'approved'
234
+ || normalized === 'rejected';
235
+ }
236
+ if (!parsed || typeof parsed !== 'object') return false;
237
+ return parsed.resolved === true
238
+ || parsed.success === true
239
+ || parsed.ok === true
240
+ || parsed.found === true;
155
241
  }
156
242
 
157
243
  async newSession(evaluate: AgentEvaluateFn): Promise<void> {
@@ -189,7 +275,15 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
189
275
  return normalized === 'true' || normalized === 'ok' || normalized === 'switched' || normalized === 'success';
190
276
  }
191
277
  if (data && typeof data === 'object') {
192
- return data.switched === true || data.success === true || data.ok === true;
278
+ if (data.switched === true || data.success === true || data.ok === true) return true;
279
+ if (typeof data.error === 'string' && data.error.trim()) return false;
280
+ }
281
+
282
+ for (let attempt = 0; attempt < 6; attempt += 1) {
283
+ await new Promise((resolve) => setTimeout(resolve, 250));
284
+ const state = await this.readChat(evaluate);
285
+ const title = typeof (state as any).title === 'string' ? (state as any).title : '';
286
+ if (this.titlesMatch(title, sessionId)) return true;
193
287
  }
194
288
  return false;
195
289
  }
@@ -1241,6 +1241,9 @@ export class ProviderCliAdapter implements CliAdapter {
1241
1241
  private looksLikeVisibleIdlePrompt(screenText: string): boolean {
1242
1242
  const text = String(screenText || '');
1243
1243
  if (!text.trim()) return false;
1244
+ if (this.cliType === 'codex-cli' && /(^|\n)\s*[❯›>]\s+(?:Find and fix a bug in @filename|Improve documentation in @filename|Use \/skills|Write tests for @filename|Explain this codebase|Summarize recent commits|Implement \{feature\}|Run \/review on my current changes)(?:\n|$)/im.test(text)) {
1245
+ return true;
1246
+ }
1244
1247
  return /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(text)
1245
1248
  || /⏎\s+send/i.test(text)
1246
1249
  || /\?\s*for\s*shortcuts/i.test(text)
@@ -115,6 +115,51 @@ function didProviderConfirmSend(result: any): boolean {
115
115
  || parsed.dispatched === true;
116
116
  }
117
117
 
118
+ async function readExtensionChatState(h: CommandHelpers): Promise<any | null> {
119
+ try {
120
+ const evalResult = await h.evaluateProviderScript('readChat', undefined, 50000);
121
+ if (!evalResult?.result) return null;
122
+ const parsed = parseMaybeJson(evalResult.result);
123
+ return parsed && typeof parsed === 'object' ? parsed : null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ function getStateMessageCount(state: any): number {
130
+ return Array.isArray(state?.messages) ? state.messages.length : 0;
131
+ }
132
+
133
+ function getStateLastSignature(state: any): string {
134
+ const messages = Array.isArray(state?.messages) ? state.messages : [];
135
+ const last = messages[messages.length - 1];
136
+ if (!last) return '';
137
+ return `${last.role || ''}:${String(last.content || '').replace(/\s+/g, ' ').trim()}`;
138
+ }
139
+
140
+ async function getStableExtensionBaseline(h: CommandHelpers): Promise<any | null> {
141
+ const first = await readExtensionChatState(h);
142
+ if (getStateMessageCount(first) > 0 || getStateLastSignature(first)) return first;
143
+ await new Promise((resolve) => setTimeout(resolve, 150));
144
+ const second = await readExtensionChatState(h);
145
+ return getStateMessageCount(second) >= getStateMessageCount(first) ? second : first;
146
+ }
147
+
148
+ async function verifyExtensionSendObserved(h: CommandHelpers, before: any): Promise<boolean> {
149
+ const beforeCount = getStateMessageCount(before);
150
+ const beforeSignature = getStateLastSignature(before);
151
+ for (let attempt = 0; attempt < 12; attempt += 1) {
152
+ await new Promise((resolve) => setTimeout(resolve, 250));
153
+ const state = await readExtensionChatState(h);
154
+ if (state?.status === 'waiting_approval') return true;
155
+ const afterCount = getStateMessageCount(state);
156
+ const afterSignature = getStateLastSignature(state);
157
+ if (afterCount > beforeCount) return true;
158
+ if (afterSignature && afterSignature !== beforeSignature) return true;
159
+ }
160
+ return false;
161
+ }
162
+
118
163
  export async function handleChatHistory(h: CommandHelpers, args: any): Promise<CommandResult> {
119
164
  const { agentType, offset, limit } = args;
120
165
  const historySessionId = getHistorySessionId(h, args);
@@ -303,12 +348,17 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
303
348
  _log(`Extension: ${provider?.type || 'unknown_extension'}`);
304
349
  // Method 1: provider sendMessage script via evaluateInSession
305
350
  try {
351
+ const beforeState = await getStableExtensionBaseline(h);
306
352
  const evalResult = await h.evaluateProviderScript('sendMessage', { message: text }, 30000);
307
353
  if (evalResult?.result) {
308
354
  const parsed = parseMaybeJson(evalResult.result);
309
355
  if (didProviderConfirmSend(parsed)) {
310
- _log(`Extension script sent OK`);
311
- return _logSendSuccess('extension-script');
356
+ const observed = await verifyExtensionSendObserved(h, beforeState);
357
+ if (observed) {
358
+ _log(`Extension script sent OK`);
359
+ return _logSendSuccess('extension-script');
360
+ }
361
+ _log(`Extension script reported send but no chat-state change was observed`);
312
362
  }
313
363
  if (parsed?.needsTypeAndSend) {
314
364
  _log(`Extension needsTypeAndSend → AgentStreamManager`);
@@ -829,7 +879,7 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
829
879
 
830
880
  // 1. Extension transport: via AgentStreamManager
831
881
  if (isExtensionTransport(transport) && h.agentStream && h.getCdp() && h.currentSession?.sessionId) {
832
- const ok = await h.agentStream.resolveSessionAction(h.getCdp()!, h.currentSession.sessionId, action);
882
+ const ok = await h.agentStream.resolveSessionAction(h.getCdp()!, h.currentSession.sessionId, action, button);
833
883
  return { success: ok };
834
884
  }
835
885
 
@@ -687,6 +687,7 @@ export class DaemonCliManager {
687
687
  if (!instanceManager) return 0;
688
688
  const sessions = records || await this.deps.listHostedCliRuntimes?.() || [];
689
689
  let restored = 0;
690
+ const restoredBindings = new Set<string>();
690
691
 
691
692
  for (const record of sessions) {
692
693
  if (!record?.runtimeId || !record?.cliType || !record?.workspace) continue;
@@ -702,6 +703,18 @@ export class DaemonCliManager {
702
703
  record.cliArgs,
703
704
  record.providerSessionId,
704
705
  );
706
+ const bindingKey = [
707
+ normalizedType,
708
+ record.workspace,
709
+ sessionBinding.providerSessionId || record.runtimeId,
710
+ ].join('::');
711
+ if (restoredBindings.has(bindingKey)) {
712
+ LOG.info(
713
+ 'CLI',
714
+ `↷ Skipping duplicate hosted runtime restore: ${record.runtimeKey || record.runtimeId} (${normalizedType} @ ${record.workspace}) binding=${sessionBinding.providerSessionId || 'runtime'}`
715
+ );
716
+ continue;
717
+ }
705
718
  try {
706
719
  await this.registerCliInstance(
707
720
  record.runtimeId,
@@ -717,6 +730,7 @@ export class DaemonCliManager {
717
730
  launchMode: 'manual',
718
731
  },
719
732
  );
733
+ restoredBindings.add(bindingKey);
720
734
  restored += 1;
721
735
  LOG.info('CLI', `♻ Restored hosted runtime: ${record.runtimeKey || record.runtimeId} (${record.displayName || record.workspace})`);
722
736
  } catch (error: any) {
@@ -44,6 +44,7 @@ export interface SessionHostControlPlane {
44
44
  restartSession(sessionId: string): Promise<any>;
45
45
  sendSignal(sessionId: string, signal: string): Promise<any>;
46
46
  forceDetachClient(sessionId: string, clientId: string): Promise<any>;
47
+ pruneDuplicateSessions(payload?: { providerType?: string; workspace?: string; dryRun?: boolean }): Promise<any>;
47
48
  acquireWrite(payload: { sessionId: string; clientId: string; ownerType: 'agent' | 'user'; force?: boolean }): Promise<any>;
48
49
  releaseWrite(payload: { sessionId: string; clientId: string }): Promise<any>;
49
50
  }
@@ -265,6 +266,16 @@ export class DaemonCommandRouter {
265
266
  return { success: true, record };
266
267
  }
267
268
 
269
+ case 'session_host_prune_duplicate_sessions': {
270
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
271
+ const result = await this.deps.sessionHostControl.pruneDuplicateSessions({
272
+ providerType: typeof args?.providerType === 'string' ? args.providerType : undefined,
273
+ workspace: typeof args?.workspace === 'string' ? args.workspace : undefined,
274
+ dryRun: args?.dryRun === true,
275
+ });
276
+ return { success: true, result };
277
+ }
278
+
268
279
  case 'session_host_acquire_write': {
269
280
  if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
270
281
  const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';