@adhdev/daemon-core 0.9.76-rc.63 → 0.9.76-rc.65

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76-rc.63",
3
+ "version": "0.9.76-rc.65",
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",
@@ -8,7 +8,7 @@ import {
8
8
  buildSessionModalDeliverySignature,
9
9
  } from './chat-signatures.js'
10
10
  import { normalizeManagedStatus } from '../status/normalize.js'
11
- import { filterUserFacingChatMessages, normalizeChatMessages } from '../providers/chat-message-normalization.js'
11
+ import { normalizeChatMessages } from '../providers/chat-message-normalization.js'
12
12
 
13
13
  export interface ChatTailSubscriptionCursor {
14
14
  tailLimit: number
@@ -103,7 +103,7 @@ export function prepareSessionChatTailUpdate(
103
103
  }
104
104
 
105
105
  const fullMessages = normalizeChatMessages(Array.isArray(result.messages) ? result.messages as any[] : [])
106
- const messages = filterUserFacingChatMessages(fullMessages)
106
+ const messages = fullMessages
107
107
  const title = typeof result.title === 'string' ? result.title : undefined
108
108
  const activeModal = normalizeChatTailActiveModal(result.activeModal)
109
109
  const status = typeof result.status === 'string' ? result.status : 'idle'
@@ -41,6 +41,8 @@ import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
41
41
  import { getSessionCompletionMarker } from '../status/snapshot.js';
42
42
  import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
43
43
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
44
+ import { homedir } from 'os';
45
+ import { join as pathJoin, resolve as pathResolve } from 'path';
44
46
 
45
47
  type ReleaseChannel = 'stable' | 'preview';
46
48
  const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
@@ -136,6 +138,36 @@ function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format:
136
138
  return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
137
139
  }
138
140
 
141
+ function resolveHermesUserHome(): string {
142
+ const explicitHome = process.env.HERMES_HOME?.trim();
143
+ return explicitHome || pathJoin(homedir(), '.hermes');
144
+ }
145
+
146
+ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
147
+ const sourceHome = resolveHermesUserHome();
148
+ const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
149
+ if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
150
+ if (pathResolve(sourceConfigPath) === pathResolve(targetConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
151
+
152
+ const parsed = parseMeshCoordinatorMcpConfig(fs.readFileSync(sourceConfigPath, 'utf-8'), 'hermes_config_yaml');
153
+ const { mcp_servers: _mcpServers, ...baseConfig } = parsed;
154
+ return { config: baseConfig, sourceHome, sourceConfigPath };
155
+ }
156
+
157
+ function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
158
+ if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
159
+ for (const fileName of ['.env', 'auth.json']) {
160
+ const sourcePath = pathJoin(sourceHome, fileName);
161
+ const targetPath = pathJoin(targetHome, fileName);
162
+ if (!fs.existsSync(sourcePath)) continue;
163
+ try {
164
+ fs.copyFileSync(sourcePath, targetPath);
165
+ } catch (error: any) {
166
+ LOG.warn('MeshCoordinator', `Could not copy Hermes ${fileName} into isolated coordinator home: ${error?.message || error}`);
167
+ }
168
+ }
169
+ }
170
+
139
171
  // ─── Types ───
140
172
 
141
173
  export interface SessionHostControlPlane {
@@ -1601,6 +1633,16 @@ export class DaemonCommandRouter {
1601
1633
  const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
1602
1634
  ? createHermesManualMeshCoordinatorSetup(meshId, workspace)
1603
1635
  : null;
1636
+ let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
1637
+ if (hermesManualFallback) {
1638
+ try {
1639
+ hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
1640
+ } catch (error: any) {
1641
+ const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
1642
+ LOG.error('MeshCoordinator', message);
1643
+ return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
1644
+ }
1645
+ }
1604
1646
  const returnManualFallback = (message: string) => ({
1605
1647
  success: false,
1606
1648
  code: 'mesh_coordinator_manual_mcp_setup_required',
@@ -1636,10 +1678,14 @@ export class DaemonCommandRouter {
1636
1678
 
1637
1679
  // Backup existing MCP config if present.
1638
1680
  const hadExistingMcpConfig = existsSync(mcpConfigPath);
1639
- let existingMcpConfig: Record<string, any> = {};
1681
+ let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
1682
+ if (hermesBaseConfig) {
1683
+ copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
1684
+ }
1640
1685
  if (hadExistingMcpConfig) {
1641
1686
  try {
1642
- existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1687
+ const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1688
+ existingMcpConfig = { ...existingMcpConfig, ...parsedExistingMcpConfig };
1643
1689
  copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1644
1690
  } catch (error: any) {
1645
1691
  LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
package/src/index.ts CHANGED
@@ -79,6 +79,10 @@ export type {
79
79
  CliProviderState,
80
80
  AcpProviderState,
81
81
  ExtensionProviderState,
82
+ MessageInputSupport,
83
+ InputMediaStrategyDescriptor,
84
+ InputAttachmentStrategy,
85
+ InputMediaType,
82
86
  } from './shared-types.js';
83
87
 
84
88
  // ── Repo Mesh Types (cross-package) ──
@@ -99,10 +99,21 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
99
99
  const workspace = readNonEmptyString(state.workspace);
100
100
  if (!workspace) return;
101
101
  const settings = state.settings && typeof state.settings === 'object' ? state.settings as Record<string, unknown> : {};
102
+
103
+ // Coordinator sessions must never inject events into themselves.
104
+ // A coordinator instance carries meshCoordinatorFor but not meshNodeFor/launchedByCoordinator.
105
+ if (readNonEmptyString(settings.meshCoordinatorFor)) return;
106
+
102
107
  const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor);
103
108
 
104
- // Prefer runtime mesh metadata: delegated mesh-node agents can come from inline/cloud meshes
105
- // that are not present in local meshes.json. Fall back to persisted mesh lookup for legacy sessions.
109
+ // Only forward events for sessions that were explicitly launched as mesh-node delegates
110
+ // (meshNodeFor set by mesh_launch_session) or that carry the launchedByCoordinator flag.
111
+ // Do NOT fall back to workspace-based mesh lookup: that would pick up coordinator sessions
112
+ // and any other CLI session that happens to share the same workspace, causing spurious
113
+ // system-message injection into the coordinator's own conversation.
114
+ const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
115
+ if (!isMeshDelegate) return;
116
+
106
117
  const mesh = meshIdFromRuntime ? getMesh(meshIdFromRuntime) : getMeshByRepo(workspace);
107
118
  const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
108
119
  if (!meshId) return;
@@ -76,9 +76,33 @@ function materializeImageDataPart(part: Extract<InputPart, { type: 'image' }>, i
76
76
  fs.mkdirSync(dir, { recursive: true });
77
77
  const filePath = path.join(dir, safeInputImageBasename(index, part.mimeType));
78
78
  fs.writeFileSync(filePath, Buffer.from(rawData, 'base64'));
79
+ cleanupStaleMaterializedImages(dir);
79
80
  return filePath;
80
81
  }
81
82
 
83
+ const MATERIALIZED_IMAGE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
84
+ const MATERIALIZED_IMAGE_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
85
+ let lastMaterializedImageCleanupAt = 0;
86
+
87
+ function cleanupStaleMaterializedImages(dir: string): void {
88
+ const now = Date.now();
89
+ if (now - lastMaterializedImageCleanupAt < MATERIALIZED_IMAGE_CLEANUP_INTERVAL_MS) return;
90
+ lastMaterializedImageCleanupAt = now;
91
+ try {
92
+ const entries = fs.readdirSync(dir);
93
+ for (const entry of entries) {
94
+ if (!entry.startsWith('adhdev-input-image-')) continue;
95
+ const fullPath = path.join(dir, entry);
96
+ try {
97
+ const stat = fs.statSync(fullPath);
98
+ if (now - stat.mtimeMs > MATERIALIZED_IMAGE_MAX_AGE_MS) {
99
+ fs.unlinkSync(fullPath);
100
+ }
101
+ } catch { /* file may have been removed concurrently */ }
102
+ }
103
+ } catch { /* dir may not exist or be inaccessible */ }
104
+ }
105
+
82
106
  export function buildCliStructuredInputPrompt(
83
107
  input: InputEnvelope,
84
108
  options: { materializeDir?: string } = {},
@@ -113,7 +137,13 @@ export function buildCliStructuredInputPrompt(
113
137
  }
114
138
  });
115
139
 
116
- if (input.textFallback.trim()) promptParts.push(input.textFallback.trim());
140
+ // Only use textFallback when no explicit text parts were collected — it is
141
+ // the flattened version of the same parts, so appending it alongside them
142
+ // would duplicate the content for multipart inputs.
143
+ const hasExplicitTextParts = input.parts.some((part) => part.type === 'text' && part.text.trim());
144
+ if (!hasExplicitTextParts && input.textFallback.trim()) {
145
+ promptParts.push(input.textFallback.trim());
146
+ }
117
147
 
118
148
  const ordered = [
119
149
  ...imageRefs,