@adhdev/daemon-core 0.8.71 → 0.8.73

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.
@@ -0,0 +1,2 @@
1
+ export declare function normalizeProviderSessionId(providerType: string | undefined, providerSessionId: string | null | undefined): string;
2
+ export declare function isLegacyVolatileSessionReadKey(key: string | null | undefined): boolean;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.71",
3
+ "version": "0.8.73",
4
4
  "description": "ADHDev local session host core — session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.71",
3
+ "version": "0.8.73",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,12 @@
16
16
  "import": "./dist/status/normalize.mjs",
17
17
  "require": "./dist/status/normalize.js",
18
18
  "default": "./dist/status/normalize.js"
19
+ },
20
+ "./chat/chat-signatures": {
21
+ "types": "./dist/chat/chat-signatures.d.ts",
22
+ "import": "./dist/chat/chat-signatures.mjs",
23
+ "require": "./dist/chat/chat-signatures.js",
24
+ "default": "./dist/chat/chat-signatures.js"
19
25
  }
20
26
  },
21
27
  "scripts": {
@@ -0,0 +1,95 @@
1
+ export interface ChatMessageSignatureInput {
2
+ id?: string | number | null
3
+ index?: number | null
4
+ role?: string | null
5
+ receivedAt?: string | number | null
6
+ timestamp?: string | number | null
7
+ content?: unknown
8
+ }
9
+
10
+ export interface ChatTailDeliverySignatureInput {
11
+ sessionId: string
12
+ historySessionId?: string
13
+ messages: unknown[]
14
+ status: string
15
+ title?: string
16
+ activeModal?: { message: string; buttons: string[] } | null
17
+ syncMode: string
18
+ replaceFrom: number
19
+ totalMessages: number
20
+ lastMessageSignature: string
21
+ }
22
+
23
+ export interface SessionModalDeliverySignatureInput {
24
+ sessionId: string
25
+ status: string
26
+ title?: string
27
+ modalMessage?: string
28
+ modalButtons?: string[]
29
+ }
30
+
31
+ export function hashSignatureParts(parts: string[]): string {
32
+ let hash = 0x811c9dc5
33
+ for (const part of parts) {
34
+ const text = String(part || '')
35
+ for (let i = 0; i < text.length; i += 1) {
36
+ hash ^= text.charCodeAt(i)
37
+ hash = Math.imul(hash, 0x01000193) >>> 0
38
+ }
39
+ hash ^= 0xff
40
+ hash = Math.imul(hash, 0x01000193) >>> 0
41
+ }
42
+ return hash.toString(16).padStart(8, '0')
43
+ }
44
+
45
+ function stringifySignatureContent(content: unknown): string {
46
+ try {
47
+ return JSON.stringify(content ?? '')
48
+ } catch {
49
+ return String(content ?? '')
50
+ }
51
+ }
52
+
53
+ function stringifySignatureMessages(messages: unknown[]): string {
54
+ try {
55
+ return JSON.stringify(messages)
56
+ } catch {
57
+ return String(messages.length)
58
+ }
59
+ }
60
+
61
+ export function buildChatMessageSignature(message: ChatMessageSignatureInput | null | undefined): string {
62
+ if (!message) return ''
63
+ return hashSignatureParts([
64
+ String(message.id || ''),
65
+ String(message.index ?? ''),
66
+ String(message.role || ''),
67
+ String(message.receivedAt ?? message.timestamp ?? ''),
68
+ stringifySignatureContent(message.content),
69
+ ])
70
+ }
71
+
72
+ export function buildChatTailDeliverySignature(payload: ChatTailDeliverySignatureInput): string {
73
+ return hashSignatureParts([
74
+ payload.sessionId,
75
+ payload.historySessionId || '',
76
+ payload.status,
77
+ payload.title || '',
78
+ payload.syncMode,
79
+ String(payload.replaceFrom),
80
+ String(payload.totalMessages),
81
+ payload.lastMessageSignature,
82
+ payload.activeModal ? `${payload.activeModal.message}|${payload.activeModal.buttons.join('\u001f')}` : '',
83
+ stringifySignatureMessages(payload.messages),
84
+ ])
85
+ }
86
+
87
+ export function buildSessionModalDeliverySignature(payload: SessionModalDeliverySignatureInput): string {
88
+ return hashSignatureParts([
89
+ payload.sessionId,
90
+ payload.status,
91
+ payload.title || '',
92
+ payload.modalMessage || '',
93
+ Array.isArray(payload.modalButtons) ? payload.modalButtons.join('\u001f') : '',
94
+ ])
95
+ }
@@ -0,0 +1,222 @@
1
+ import type {
2
+ ReadChatCursor,
3
+ ReadChatSyncMode,
4
+ ReadChatSyncResult,
5
+ SessionChatTailUpdate,
6
+ SessionModalUpdate,
7
+ } from '../shared-types.js'
8
+ import {
9
+ buildChatTailDeliverySignature,
10
+ buildSessionModalDeliverySignature,
11
+ } from './chat-signatures.js'
12
+
13
+ export interface ChatTailSubscriptionCursor {
14
+ knownMessageCount: number
15
+ lastMessageSignature: string
16
+ tailLimit: number
17
+ }
18
+
19
+ export type SessionChatTailCommandResult = Partial<Omit<ReadChatSyncResult, 'activeModal'>> & {
20
+ success?: boolean
21
+ activeModal?: unknown
22
+ }
23
+
24
+ export interface PrepareSessionChatTailUpdateInput {
25
+ key: string
26
+ sessionId: string
27
+ historySessionId?: string
28
+ seq: number
29
+ timestamp: number
30
+ interactionId?: string
31
+ cursor: ChatTailSubscriptionCursor
32
+ lastDeliveredSignature: string
33
+ result: SessionChatTailCommandResult | null | undefined
34
+ }
35
+
36
+ export interface PreparedSessionChatTailUpdate {
37
+ cursor: ChatTailSubscriptionCursor
38
+ seq: number
39
+ lastDeliveredSignature: string
40
+ update: SessionChatTailUpdate | null
41
+ }
42
+
43
+ export interface PrepareSessionModalUpdateInput {
44
+ key: string
45
+ sessionId: string
46
+ status: string
47
+ title?: string
48
+ activeModal?: unknown
49
+ seq: number
50
+ timestamp: number
51
+ interactionId?: string
52
+ lastDeliveredSignature: string
53
+ }
54
+
55
+ export interface PreparedSessionModalUpdate {
56
+ seq: number
57
+ lastDeliveredSignature: string
58
+ update: SessionModalUpdate | null
59
+ }
60
+
61
+ function normalizeSyncMode(syncMode: string | undefined): ReadChatSyncMode {
62
+ return syncMode === 'append'
63
+ || syncMode === 'replace_tail'
64
+ || syncMode === 'noop'
65
+ || syncMode === 'full'
66
+ ? syncMode
67
+ : 'full'
68
+ }
69
+
70
+ function normalizeModalButtons(value: unknown): string[] {
71
+ return Array.isArray(value)
72
+ ? value.filter((button): button is string => typeof button === 'string')
73
+ : []
74
+ }
75
+
76
+ function normalizeModalMessage(value: unknown): string | undefined {
77
+ return typeof value === 'string' ? value : undefined
78
+ }
79
+
80
+ export function normalizeChatTailActiveModal(activeModal: unknown): { message: string; buttons: string[] } | null {
81
+ if (!activeModal || typeof activeModal !== 'object') return null
82
+ const message = normalizeModalMessage((activeModal as { message?: unknown }).message)
83
+ if (!message) return null
84
+ const rawButtons = (activeModal as { buttons?: unknown }).buttons
85
+ if (!Array.isArray(rawButtons)) return null
86
+ return {
87
+ message,
88
+ buttons: normalizeModalButtons(rawButtons),
89
+ }
90
+ }
91
+
92
+ export function normalizeSessionModalFields(activeModal: unknown): { modalMessage?: string; modalButtons: string[] } {
93
+ if (!activeModal || typeof activeModal !== 'object') {
94
+ return { modalButtons: [] }
95
+ }
96
+
97
+ return {
98
+ modalMessage: normalizeModalMessage((activeModal as { message?: unknown }).message),
99
+ modalButtons: normalizeModalButtons((activeModal as { buttons?: unknown }).buttons),
100
+ }
101
+ }
102
+
103
+ function buildNextChatCursor(
104
+ cursor: ChatTailSubscriptionCursor,
105
+ result: SessionChatTailCommandResult,
106
+ ): ChatTailSubscriptionCursor {
107
+ return {
108
+ knownMessageCount: Math.max(0, Number(result.totalMessages || cursor.knownMessageCount)),
109
+ lastMessageSignature: typeof result.lastMessageSignature === 'string'
110
+ ? result.lastMessageSignature
111
+ : cursor.lastMessageSignature,
112
+ tailLimit: cursor.tailLimit,
113
+ }
114
+ }
115
+
116
+ export function prepareSessionChatTailUpdate(
117
+ input: PrepareSessionChatTailUpdateInput,
118
+ ): PreparedSessionChatTailUpdate {
119
+ const result = input.result
120
+ if (!result?.success || result.syncMode === 'noop') {
121
+ return {
122
+ cursor: result?.success ? buildNextChatCursor(input.cursor, result) : input.cursor,
123
+ seq: input.seq,
124
+ lastDeliveredSignature: input.lastDeliveredSignature,
125
+ update: null,
126
+ }
127
+ }
128
+
129
+ const syncMode = normalizeSyncMode(result.syncMode)
130
+ const cursor = {
131
+ knownMessageCount: Math.max(0, Number(result.totalMessages || 0)),
132
+ lastMessageSignature: typeof result.lastMessageSignature === 'string' ? result.lastMessageSignature : '',
133
+ tailLimit: input.cursor.tailLimit,
134
+ }
135
+ const title = typeof result.title === 'string' ? result.title : undefined
136
+ const activeModal = normalizeChatTailActiveModal(result.activeModal)
137
+ const status = typeof result.status === 'string' ? result.status : 'idle'
138
+ const deliverySignature = buildChatTailDeliverySignature({
139
+ sessionId: input.sessionId,
140
+ ...(input.historySessionId ? { historySessionId: input.historySessionId } : {}),
141
+ messages: Array.isArray(result.messages) ? result.messages : [],
142
+ status,
143
+ ...(title ? { title } : {}),
144
+ ...(activeModal ? { activeModal } : {}),
145
+ syncMode,
146
+ replaceFrom: Number(result.replaceFrom || 0),
147
+ totalMessages: Number(result.totalMessages || 0),
148
+ lastMessageSignature: typeof result.lastMessageSignature === 'string' ? result.lastMessageSignature : '',
149
+ })
150
+ const seq = input.seq + 1
151
+
152
+ if (deliverySignature === input.lastDeliveredSignature) {
153
+ return {
154
+ cursor,
155
+ seq,
156
+ lastDeliveredSignature: input.lastDeliveredSignature,
157
+ update: null,
158
+ }
159
+ }
160
+
161
+ return {
162
+ cursor,
163
+ seq,
164
+ lastDeliveredSignature: deliverySignature,
165
+ update: {
166
+ topic: 'session.chat_tail',
167
+ key: input.key,
168
+ sessionId: input.sessionId,
169
+ ...(input.historySessionId ? { historySessionId: input.historySessionId } : {}),
170
+ ...(input.interactionId ? { interactionId: input.interactionId } : {}),
171
+ seq,
172
+ timestamp: input.timestamp,
173
+ messages: Array.isArray(result.messages) ? result.messages : [],
174
+ status,
175
+ ...(title ? { title } : {}),
176
+ ...(activeModal ? { activeModal } : {}),
177
+ syncMode,
178
+ replaceFrom: Number(result.replaceFrom || 0),
179
+ totalMessages: Number(result.totalMessages || 0),
180
+ lastMessageSignature: typeof result.lastMessageSignature === 'string' ? result.lastMessageSignature : '',
181
+ },
182
+ }
183
+ }
184
+
185
+ export function prepareSessionModalUpdate(
186
+ input: PrepareSessionModalUpdateInput,
187
+ ): PreparedSessionModalUpdate {
188
+ const { modalMessage, modalButtons } = normalizeSessionModalFields(input.activeModal)
189
+ const deliverySignature = buildSessionModalDeliverySignature({
190
+ sessionId: input.sessionId,
191
+ status: input.status,
192
+ ...(input.title ? { title: input.title } : {}),
193
+ ...(modalMessage ? { modalMessage } : {}),
194
+ ...(modalButtons.length > 0 ? { modalButtons } : {}),
195
+ })
196
+
197
+ if (deliverySignature === input.lastDeliveredSignature) {
198
+ return {
199
+ seq: input.seq,
200
+ lastDeliveredSignature: input.lastDeliveredSignature,
201
+ update: null,
202
+ }
203
+ }
204
+
205
+ const seq = input.seq + 1
206
+ return {
207
+ seq,
208
+ lastDeliveredSignature: deliverySignature,
209
+ update: {
210
+ topic: 'session.modal',
211
+ key: input.key,
212
+ sessionId: input.sessionId,
213
+ status: input.status,
214
+ ...(input.title ? { title: input.title } : {}),
215
+ ...(modalMessage ? { modalMessage } : {}),
216
+ ...(modalButtons.length > 0 ? { modalButtons } : {}),
217
+ ...(input.interactionId ? { interactionId: input.interactionId } : {}),
218
+ seq,
219
+ timestamp: input.timestamp,
220
+ },
221
+ }
222
+ }
@@ -12,6 +12,7 @@ import type { ProviderInstance } from '../providers/provider-instance.js';
12
12
  import { readChatHistory } from '../config/chat-history.js';
13
13
  import { LOG } from '../logging/logger.js';
14
14
  import { recordDebugTrace } from '../logging/debug-trace.js';
15
+ import { buildChatMessageSignature } from '../chat/chat-signatures.js';
15
16
  import type { ChatMessage } from '../types.js';
16
17
  import type { ReadChatCursor, ReadChatSyncMode, SessionTransport } from '../shared-types.js';
17
18
  import { normalizeChatMessages } from '../providers/chat-message-normalization.js';
@@ -19,20 +20,6 @@ import { normalizeChatMessages } from '../providers/chat-message-normalization.j
19
20
  const RECENT_SEND_WINDOW_MS = 1200;
20
21
  const recentSendByTarget = new Map<string, number>();
21
22
 
22
- function hashSignatureParts(parts: string[]): string {
23
- let hash = 0x811c9dc5;
24
- for (const part of parts) {
25
- const text = String(part || '');
26
- for (let i = 0; i < text.length; i += 1) {
27
- hash ^= text.charCodeAt(i);
28
- hash = Math.imul(hash, 0x01000193) >>> 0;
29
- }
30
- hash ^= 0xff;
31
- hash = Math.imul(hash, 0x01000193) >>> 0;
32
- }
33
- return hash.toString(16).padStart(8, '0');
34
- }
35
-
36
23
  interface ApprovalSelectableInstance extends ProviderInstance {
37
24
  recordApprovalSelection?(buttonText: string): void;
38
25
  }
@@ -171,20 +158,7 @@ function parseMaybeJson(value: any): any {
171
158
  }
172
159
 
173
160
  function getChatMessageSignature(message: ChatMessage | null | undefined): string {
174
- if (!message) return '';
175
- let content = '';
176
- try {
177
- content = JSON.stringify(message.content ?? '');
178
- } catch {
179
- content = String(message.content ?? '');
180
- }
181
- return hashSignatureParts([
182
- String(message.id || ''),
183
- String(message.index ?? ''),
184
- String(message.role || ''),
185
- String(message.receivedAt ?? message.timestamp ?? ''),
186
- content,
187
- ]);
161
+ return buildChatMessageSignature(message);
188
162
  }
189
163
 
190
164
  function normalizeReadChatCursor(args: any): Required<ReadChatCursor> {
@@ -24,6 +24,7 @@ import { ChatHistoryWriter } from '../config/chat-history.js';
24
24
  import type { SessionRegistry, SessionRuntimeTarget } from '../sessions/registry.js';
25
25
  import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
26
26
  import { LOG } from '../logging/logger.js';
27
+ import { resolveLegacyProviderScript, type LegacyStringScript } from './provider-script-resolver.js';
27
28
 
28
29
  // Sub-module imports
29
30
  import * as Chat from './chat-commands.js';
@@ -68,8 +69,6 @@ export interface CommandHelpers {
68
69
  readonly historyWriter: ChatHistoryWriter;
69
70
  }
70
71
 
71
- type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
72
-
73
72
  const COMMAND_DEBUG_LEVELS = new Set([
74
73
  'pty_input',
75
74
  'pty_resize',
@@ -220,32 +219,7 @@ export class DaemonCommandHandler implements CommandHelpers {
220
219
  if (provider?.scripts) {
221
220
  const fn = provider.scripts[scriptName];
222
221
  if (typeof fn === 'function') {
223
- const callScript = fn as LegacyStringScript;
224
- if (params && Object.keys(params).length > 0) {
225
- const firstVal = Object.values(params)[0];
226
- if (scriptName === 'sendMessage' && typeof firstVal === 'string') {
227
- const legacyScript = callScript(firstVal);
228
- if (legacyScript) return legacyScript;
229
- }
230
- const script = callScript(params);
231
- if (script) {
232
- const likelyLegacyObjectLeak =
233
- typeof script === 'string'
234
- && script.includes('[object Object]')
235
- && typeof firstVal === 'string';
236
- if (!likelyLegacyObjectLeak) return script;
237
- }
238
-
239
- if (firstVal !== undefined) {
240
- const legacyScript = callScript(firstVal);
241
- if (legacyScript) return legacyScript;
242
- }
243
-
244
- if (script) return script;
245
- } else {
246
- const script = callScript();
247
- if (script) return script;
248
- }
222
+ return resolveLegacyProviderScript(fn as LegacyStringScript, scriptName, params);
249
223
  }
250
224
  }
251
225
  return null;
@@ -0,0 +1,40 @@
1
+ export type LegacyStringScript = (params?: Record<string, unknown> | string) => string | null | undefined
2
+
3
+ export function resolveLegacyProviderScript(
4
+ fn: LegacyStringScript | null | undefined,
5
+ scriptName: string,
6
+ params?: Record<string, unknown> | string,
7
+ ): string | null {
8
+ if (typeof fn !== 'function') return null
9
+
10
+ if (params && typeof params === 'object' && !Array.isArray(params) && Object.keys(params).length > 0) {
11
+ const firstVal = Object.values(params)[0]
12
+
13
+ if (scriptName === 'sendMessage' && typeof firstVal === 'string') {
14
+ const legacyScript = fn(firstVal)
15
+ if (legacyScript) return legacyScript
16
+ }
17
+
18
+ const script = fn(params)
19
+ const likelyLegacyObjectLeak =
20
+ typeof script === 'string'
21
+ && script.includes('[object Object]')
22
+ && typeof firstVal === 'string'
23
+ if (!likelyLegacyObjectLeak && script) return script
24
+
25
+ if (firstVal !== undefined) {
26
+ const legacyScript = fn(firstVal as string)
27
+ if (legacyScript) return legacyScript
28
+ }
29
+
30
+ if (script) return script
31
+ return null
32
+ }
33
+
34
+ if (params !== undefined) {
35
+ const script = fn(params)
36
+ if (script) return script
37
+ }
38
+
39
+ return fn() || null
40
+ }
@@ -12,6 +12,7 @@ import { join } from 'path';
12
12
  import { getConfigDir } from './config.js';
13
13
  import type { RecentActivityEntry } from './recent-activity.js';
14
14
  import type { SavedProviderSessionEntry } from './saved-sessions.js';
15
+ import { isLegacyVolatileSessionReadKey, normalizeProviderSessionId } from '../providers/provider-session-id.js';
15
16
 
16
17
  export interface DaemonState {
17
18
  /** Unified recent activity across IDE / CLI / ACP launch flows */
@@ -42,18 +43,38 @@ function getStatePath(): string {
42
43
  function normalizeState(raw: unknown): DaemonState {
43
44
  const parsed = isPlainObject(raw) ? raw : {};
44
45
 
46
+ const recentActivity = (Array.isArray(parsed.recentActivity) ? parsed.recentActivity : [])
47
+ .filter((entry): entry is RecentActivityEntry => {
48
+ if (!isPlainObject(entry)) return false;
49
+ const normalizedId = normalizeProviderSessionId(
50
+ typeof entry.providerType === 'string' ? entry.providerType : '',
51
+ typeof entry.providerSessionId === 'string' ? entry.providerSessionId : '',
52
+ );
53
+ if (typeof entry.providerSessionId === 'string' && !normalizedId) return false;
54
+ return true;
55
+ });
56
+
57
+ const savedProviderSessions = (Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions : [])
58
+ .filter((entry): entry is SavedProviderSessionEntry => {
59
+ if (!isPlainObject(entry)) return false;
60
+ return !!normalizeProviderSessionId(
61
+ typeof entry.providerType === 'string' ? entry.providerType : '',
62
+ typeof entry.providerSessionId === 'string' ? entry.providerSessionId : '',
63
+ );
64
+ });
65
+
45
66
  const sessionReads = Object.fromEntries(
46
67
  Object.entries(isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {})
47
- .filter(([, value]) => typeof value === 'number' && Number.isFinite(value as number))
68
+ .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'number' && Number.isFinite(value as number))
48
69
  );
49
70
  const sessionReadMarkers = Object.fromEntries(
50
71
  Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
51
- .filter(([, value]) => typeof value === 'string')
72
+ .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'string')
52
73
  );
53
74
 
54
75
  return {
55
- recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
56
- savedProviderSessions: Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions as SavedProviderSessionEntry[] : [],
76
+ recentActivity,
77
+ savedProviderSessions,
57
78
  sessionReads,
58
79
  sessionReadMarkers,
59
80
  };
@@ -33,6 +33,7 @@ import { VersionArchive, detectAllVersions } from '../providers/version-archive.
33
33
  import { LOG } from '../logging/logger.js';
34
34
  import { findCdpManager } from '../status/builders.js';
35
35
  import { handleCdpEvaluate, handleCdpClick, handleCdpDomQuery, handleScreenshot, handleScriptsRun, handleTypeAndSend, handleTypeAndSendAt, handleScriptHints, handleCdpTargets, handleDomInspect, handleDomChildren, handleDomAnalyze, handleFindCommon, handleFindByText, handleDomContext } from './dev-cdp-handlers.js';
36
+ import { resolveLegacyProviderScript, type LegacyStringScript } from '../commands/provider-script-resolver.js';
36
37
  import { handleCliStatus, handleCliLaunch, handleCliSend, handleCliStop, handleCliDebug, handleCliTrace, handleCliExercise, handleCliFixtureCapture, handleCliFixtureList, handleCliFixtureReplay, handleCliResolve, handleCliRaw, handleCliSSE } from './dev-cli-debug.js';
37
38
  import { handleAutoImplement, handleAutoImplCancel, handleAutoImplSSE } from './dev-auto-implement.js';
38
39
 
@@ -386,7 +387,8 @@ export class DevServer implements DevServerContext {
386
387
 
387
388
  public async handleRunScript(type: string, req: http.IncomingMessage, res: http.ServerResponse, parsedBody?: any): Promise<void> {
388
389
  const body = parsedBody || await this.readBody(req);
389
- const { script: scriptName, params, ideType: scriptIdeType } = body;
390
+ const { script: scriptName, params, args, ideType: scriptIdeType } = body;
391
+ const rawParams = args !== undefined ? args : params;
390
392
 
391
393
  const provider = this.providerLoader.resolve(type);
392
394
  if (!provider) {
@@ -407,18 +409,7 @@ export class DevServer implements DevServerContext {
407
409
  }
408
410
 
409
411
  try {
410
- // Emulate production CommandHandler behavior
411
- let scriptCode: string | null = null;
412
- if (['sendMessage', 'webviewSendMessage', 'switchSession', 'webviewSwitchSession', 'setMode', 'webviewSetMode', 'setModel', 'webviewSetModel'].includes(scriptName)) {
413
- // Production daemon's getProviderScript always unpacks the object and sends the first value
414
- const firstVal = params && typeof params === 'object' && Object.keys(params).length > 0
415
- ? Object.values(params)[0]
416
- : params;
417
- scriptCode = firstVal !== undefined ? fn(firstVal) : fn();
418
- } else {
419
- // Scripts like resolveAction are passed the raw parameters object in production
420
- scriptCode = params !== undefined ? fn(params) : fn();
421
- }
412
+ const scriptCode = resolveLegacyProviderScript(fn as LegacyStringScript, scriptName, rawParams);
422
413
  if (!scriptCode) {
423
414
  this.json(res, 500, { error: 'Script function returned null' });
424
415
  return;
@@ -429,12 +420,23 @@ export class DevServer implements DevServerContext {
429
420
  const isWebviewScript = scriptName.toLowerCase().includes('webview');
430
421
  let raw: any;
431
422
  if (provider.category === 'extension' && !isWebviewScript) {
432
- // Extension scripts: prefer session frame (agent webview) — matching agent-stream poller behavior
423
+ // Extension scripts: prefer the requested agent webview session.
433
424
  const sessions = cdp.getAgentSessions();
434
425
  let sessionId: string | null = null;
435
426
  for (const [sid, target] of sessions) {
436
427
  if (target.agentType === type) { sessionId = sid; break; }
437
428
  }
429
+ if (!sessionId) {
430
+ try {
431
+ const discovered = await cdp.discoverAgentWebviews();
432
+ const target = discovered.find((entry) => entry.agentType === type);
433
+ if (target) {
434
+ sessionId = await cdp.attachToAgent(target);
435
+ }
436
+ } catch (error) {
437
+ this.log(`Extension attach fallback failed for ${type}: ${(error as Error)?.message || String(error)}`);
438
+ }
439
+ }
438
440
  if (sessionId) {
439
441
  raw = await cdp.evaluateInSessionFrame(sessionId, scriptCode);
440
442
  } else if (cdp.evaluateInWebviewFrame) {
package/src/index.ts CHANGED
@@ -188,6 +188,31 @@ export { DEFAULT_DAEMON_PORT, DAEMON_WS_PATH } from './ipc-protocol.js';
188
188
 
189
189
  // ── Chat History ──
190
190
  export { readChatHistory } from './config/chat-history.js';
191
+ export {
192
+ hashSignatureParts,
193
+ buildChatMessageSignature,
194
+ buildChatTailDeliverySignature,
195
+ buildSessionModalDeliverySignature,
196
+ } from './chat/chat-signatures.js';
197
+ export type {
198
+ ChatMessageSignatureInput,
199
+ ChatTailDeliverySignatureInput,
200
+ SessionModalDeliverySignatureInput,
201
+ } from './chat/chat-signatures.js';
202
+ export {
203
+ normalizeChatTailActiveModal,
204
+ normalizeSessionModalFields,
205
+ prepareSessionChatTailUpdate,
206
+ prepareSessionModalUpdate,
207
+ } from './chat/subscription-updates.js';
208
+ export type {
209
+ ChatTailSubscriptionCursor,
210
+ PrepareSessionChatTailUpdateInput,
211
+ PreparedSessionChatTailUpdate,
212
+ PrepareSessionModalUpdateInput,
213
+ PreparedSessionModalUpdate,
214
+ SessionChatTailCommandResult,
215
+ } from './chat/subscription-updates.js';
191
216
 
192
217
  // ── Agent Stream ──
193
218
  export { DaemonAgentStreamManager } from './agent-stream/index.js';
@@ -24,6 +24,7 @@ import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from '.
24
24
  import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
25
25
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
26
26
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
27
+ import { normalizeProviderSessionId } from './provider-session-id.js';
27
28
  import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
28
29
 
29
30
  let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
@@ -304,9 +305,10 @@ export class CliProviderInstance implements ProviderInstance {
304
305
  const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
305
306
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
306
307
  const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
307
- const parsedProviderSessionId = typeof parsedStatus?.providerSessionId === 'string'
308
- ? parsedStatus.providerSessionId.trim()
309
- : '';
308
+ const parsedProviderSessionId = normalizeProviderSessionId(
309
+ this.type,
310
+ typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
311
+ );
310
312
  if (parsedProviderSessionId) {
311
313
  this.promoteProviderSessionId(parsedProviderSessionId);
312
314
  }
@@ -598,9 +600,10 @@ export class CliProviderInstance implements ProviderInstance {
598
600
  private applyProviderResponse(data: any, options: { phase: 'immediate' | 'turn_completed' }): void {
599
601
  if (!data || typeof data !== 'object') return;
600
602
 
601
- const patchedProviderSessionId = typeof data.providerSessionId === 'string'
602
- ? data.providerSessionId.trim()
603
- : '';
603
+ const patchedProviderSessionId = normalizeProviderSessionId(
604
+ this.type,
605
+ typeof data.providerSessionId === 'string' ? data.providerSessionId : '',
606
+ );
604
607
  if (patchedProviderSessionId) {
605
608
  this.promoteProviderSessionId(patchedProviderSessionId);
606
609
  }