@adhdev/daemon-core 0.9.82-rc.321 → 0.9.82-rc.322

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.
@@ -302,6 +302,15 @@ export interface SessionEntry {
302
302
  seenCompletionMarker?: string;
303
303
  surfaceHidden?: boolean;
304
304
  settings?: Record<string, any>;
305
+ /**
306
+ * True owning-daemon id for a session a coordinator synthesises into its own
307
+ * status snapshot (mesh delegated sessions). The dashboard attributes the session
308
+ * to this daemon instead of the snapshot daemon so the worker node — not the
309
+ * coordinator — is shown as its machine.
310
+ */
311
+ ownerDaemonId?: string;
312
+ /** True owning-machine display name fallback when the owning daemon is not aggregated. */
313
+ ownerMachineName?: string;
305
314
  /** Set when this session is acting as a mesh coordinator for the given mesh. */
306
315
  coordinator?: {
307
316
  meshId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.321",
3
+ "version": "0.9.82-rc.322",
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",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.321",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.322",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -85,6 +85,7 @@ import { basename as pathBasename, join as pathJoin, resolve as pathResolve } fr
85
85
  import * as fs from 'fs';
86
86
  import { execFileSync } from 'node:child_process';
87
87
  import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
88
+ import { workingDirBasename } from '../providers/working-dir.js';
88
89
 
89
90
  type ReleaseChannel = 'stable' | 'preview';
90
91
  const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
@@ -223,11 +224,14 @@ function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>):
223
224
  // joinRepoPath + readGitSubmodules moved to @adhdev/mesh-shared (readGitSubmodules)
224
225
  // — used via sharedNormalizeGitStatus / sharedPickBestTransitGitStatus below.
225
226
 
226
- function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
227
+ export function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
227
228
  const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
228
229
  if (explicit) return explicit;
229
230
  const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
230
- const workspaceName = workspace ? pathBasename(workspace) : undefined;
231
+ // Use the OS-agnostic basename: a workspace reported by a Windows node
232
+ // (`D:\gh\adhdev-cloud`) must still collapse to its trailing segment even when
233
+ // this coordinator's own `path.basename` is POSIX-only and would not split `\`.
234
+ const workspaceName = workspace ? workingDirBasename(workspace) : undefined;
231
235
  const host = readStringValue(node.machineName, node.machine_name, node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
232
236
  const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
233
237
  const parts = [workspaceName, host, provider].filter(Boolean);
@@ -873,6 +877,22 @@ function readCachedInlineMeshActiveSessions(node: any): string[] {
873
877
  return sessionId ? [sessionId] : [];
874
878
  }
875
879
 
880
+ /**
881
+ * Resolve the owning-node attribution for a mesh node record so a coordinator can
882
+ * stamp the TRUE owner onto a synthetic session entry instead of letting the
883
+ * dashboard fall back to the coordinator's own daemonId. Returns whichever of the
884
+ * owning node's `daemonId` / display machine name could be read from the node's
885
+ * (possibly multi-serialization-path) shape; both may be undefined for a node that
886
+ * never carried machine identity.
887
+ */
888
+ export function resolveMeshNodeAttribution(node: unknown): { daemonId?: string; machineName?: string } {
889
+ const record = readObjectRecord(node);
890
+ return {
891
+ daemonId: readMeshNodeDaemonId(record),
892
+ machineName: readMeshNodeDisplayMachineName(record),
893
+ };
894
+ }
895
+
876
896
  export function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
877
897
  const cachedStatus = readObjectRecord(node?.cachedStatus);
878
898
  const activeSession = readObjectRecord(cachedStatus.activeSession);
@@ -36,12 +36,71 @@ function loadMeshConfig(): LocalMeshConfig {
36
36
  try {
37
37
  const raw = JSON.parse(readFileSync(path, 'utf-8'));
38
38
  if (!raw || !Array.isArray(raw.meshes)) return { meshes: [] };
39
- return raw as LocalMeshConfig;
39
+ const config = raw as LocalMeshConfig;
40
+ const migrated = migrateLoadedMeshConfig(config);
41
+ // Persist eagerly when the on-load migration changed anything, so the
42
+ // dead field is gone from disk even on a pure-read path (mesh_status /
43
+ // mesh_list_nodes) that never otherwise mutates the config. Best-effort:
44
+ // a write failure (e.g. read-only fs) must not break reads, so swallow.
45
+ if (migrated) {
46
+ try {
47
+ saveMeshConfig(config);
48
+ } catch {
49
+ // keep the in-memory strip; disk converges on the next mutating op
50
+ }
51
+ }
52
+ return config;
40
53
  } catch {
41
54
  return { meshes: [] };
42
55
  }
43
56
  }
44
57
 
58
+ /**
59
+ * In-place migration applied to every loaded meshes.json. Strips data that
60
+ * outlived the feature that wrote it so the persisted config converges on the
61
+ * current schema the next time it is saved.
62
+ *
63
+ * Currently: drops the dead `role` field from each node policy's providerRoles
64
+ * entries. providerRoles is retained for its `maxParallel` per-(node, provider)
65
+ * cap, but the routing `role` was removed (routing is governed solely by
66
+ * required_tags). A meshes.json written before the removal still carries
67
+ * `role: "validator"` etc.; this drops it on load so mesh_status / mesh_list_nodes
68
+ * never surface the dead field and the next saveMeshConfig() persists it gone.
69
+ *
70
+ * Returns true when the config was mutated (caller may persist eagerly).
71
+ */
72
+ function migrateLoadedMeshConfig(config: LocalMeshConfig): boolean {
73
+ let changed = false;
74
+ for (const mesh of config.meshes) {
75
+ if (!mesh || !Array.isArray(mesh.nodes)) continue;
76
+ for (const node of mesh.nodes) {
77
+ if (stripDeadRoleFromProviderRoles(node?.policy)) changed = true;
78
+ }
79
+ }
80
+ return changed;
81
+ }
82
+
83
+ /**
84
+ * Drop the legacy `role` field from each providerRoles entry of a node policy,
85
+ * in place. Keeps providerType + maxParallel (the still-meaningful per-(node,
86
+ * provider) cap). Defensive against malformed entries — non-object items are
87
+ * left untouched. Returns true when at least one `role` field was removed.
88
+ */
89
+ function stripDeadRoleFromProviderRoles(policy: unknown): boolean {
90
+ if (!policy || typeof policy !== 'object') return false;
91
+ const roles = (policy as { providerRoles?: unknown }).providerRoles;
92
+ if (!Array.isArray(roles)) return false;
93
+ let changed = false;
94
+ for (const entry of roles) {
95
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)
96
+ && Object.prototype.hasOwnProperty.call(entry, 'role')) {
97
+ delete (entry as Record<string, unknown>).role;
98
+ changed = true;
99
+ }
100
+ }
101
+ return changed;
102
+ }
103
+
45
104
  function normalizeCapabilityTags(value: unknown): string[] | undefined {
46
105
  if (!Array.isArray(value)) return undefined;
47
106
  const seen = new Set<string>();
package/src/index.ts CHANGED
@@ -300,7 +300,7 @@ export type { CdpInitializerConfig } from './cdp/initializer.js';
300
300
  // ── Commands ──
301
301
  export { DaemonCommandHandler } from './commands/handler.js';
302
302
  export type { CommandResult, CommandContext } from './commands/handler.js';
303
- export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails } from './commands/router.js';
303
+ export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails, resolveMeshNodeAttribution } from './commands/router.js';
304
304
  export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
305
305
  export {
306
306
  maybeRunDaemonUpgradeHelperFromEnv,
@@ -206,6 +206,14 @@ export interface SessionEntry {
206
206
  completionMarker?: string;
207
207
  seenCompletionMarker?: string;
208
208
  surfaceHidden?: boolean;
209
+ /**
210
+ * True owning-daemon id for a session a coordinator synthesises into its own
211
+ * status snapshot (mesh delegated sessions), so the dashboard attributes it to the
212
+ * worker node instead of the coordinator daemon hosting the snapshot.
213
+ */
214
+ ownerDaemonId?: string;
215
+ /** True owning-machine display name fallback when the owning daemon is not aggregated. */
216
+ ownerMachineName?: string;
209
217
  }
210
218
  /**
211
219
  * Compact session metadata stored in UserSessionDO and reused by server-side
@@ -399,6 +399,15 @@ export interface SessionEntry {
399
399
  seenCompletionMarker?: string;
400
400
  surfaceHidden?: boolean;
401
401
  settings?: Record<string, any>;
402
+ /**
403
+ * True owning-daemon id for a session a coordinator synthesises into its own
404
+ * status snapshot (mesh delegated sessions). The dashboard attributes the session
405
+ * to this daemon instead of the snapshot daemon so the worker node — not the
406
+ * coordinator — is shown as its machine.
407
+ */
408
+ ownerDaemonId?: string;
409
+ /** True owning-machine display name fallback when the owning daemon is not aggregated. */
410
+ ownerMachineName?: string;
402
411
  /** Set when this session is acting as a mesh coordinator for the given mesh. */
403
412
  coordinator?: { meshId: string; role: 'coordinator' };
404
413
  meshQueueStats?: {