@adhdev/daemon-core 0.9.82-rc.547 → 0.9.82-rc.549

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.82-rc.547",
3
+ "version": "0.9.82-rc.549",
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",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "0.9.82-rc.547",
51
- "@adhdev/session-host-core": "0.9.82-rc.547",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.549",
51
+ "@adhdev/session-host-core": "0.9.82-rc.549",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -14,10 +14,81 @@ import { loadState } from '../../config/state-store.js';
14
14
  import { getRecentActivity } from '../../config/recent-activity.js';
15
15
  import { getSavedProviderSessions } from '../../config/saved-sessions.js';
16
16
  import { listProviderHistorySessions } from '../../config/chat-history.js';
17
- import { buildMeshWorkerRelayStamp } from '../../mesh/mesh-events-utils.js';
17
+ import { buildMeshWorkerRelayStamp, readNonEmptyString } from '../../mesh/mesh-events-utils.js';
18
+ import { LOG } from '../../logging/logger.js';
18
19
  import { readStringValue } from '../router.js';
19
20
  import type { MedFamilyContext, MedFamilyHandler } from './types.js';
20
21
 
22
+ /** Outcome of the coordinator-mirror refresh: not a mesh worker (no-op), refreshed, or failed. */
23
+ type MirrorForwardOutcome = 'skipped' | 'refreshed' | 'failed';
24
+
25
+ /**
26
+ * MESH-WORKER-PREFS Fix B: when a mesh WORKER session's per-conversation Hide/Mute is toggled
27
+ * (set_conversation_prefs, forwarded to the worker daemon by the coordinator's
28
+ * MESH_FORWARDABLE_SESSION_COMMANDS router), the worker updates its own live-session
29
+ * userHidden/userMuted but the COORDINATOR's mirror of that session (adhdev-daemon
30
+ * meshOwnedSessions) is only refreshed by a `mesh_forward_event` carrying `sessionSettings`.
31
+ * Without one the mirror stays stale, so when the coordinator re-emits the session metadata to
32
+ * the dashboard the old surfaceHidden/muted comes back and the toggle reverts after the 8s
33
+ * optimistic overlay expires. Push a `mesh_forward_event` to the coordinator daemon with the
34
+ * fresh prefs so its mirror updates immediately. The coordinator's mesh_forward_event command
35
+ * consumer calls updateMeshOwnedSession(args) BEFORE routing, so the mirror is refreshed even
36
+ * though the (non coordinator-event) event name is otherwise ignored.
37
+ *
38
+ * Fix (4) — atomicity: the worker's local updateSettings and the coordinator mirror refresh were
39
+ * fire-and-forget, so a dropped/rejected forward left the mirror stale and the dashboard toggle
40
+ * silently reverted (the exact "restore does nothing" defect). This now AWAITS the dispatch, retries
41
+ * once on failure, and reports the outcome so the handler can flag a stale mirror to the caller
42
+ * instead of returning an unqualified success. It never rejects — a failure is reported, not thrown,
43
+ * so the local write (already applied) still returns success with a mirrorStale marker.
44
+ */
45
+ async function forwardConversationPrefsToCoordinator(
46
+ ctx: MedFamilyContext,
47
+ sessionId: string,
48
+ patch: Record<string, unknown>,
49
+ ): Promise<MirrorForwardOutcome> {
50
+ const dispatch = ctx.deps.dispatchMeshCommand;
51
+ if (!dispatch) return 'skipped';
52
+ let settings: Record<string, unknown> = {};
53
+ try {
54
+ const state = ctx.deps.instanceManager.getInstance(sessionId)?.getState?.();
55
+ if (state?.settings && typeof state.settings === 'object') {
56
+ settings = state.settings as Record<string, unknown>;
57
+ }
58
+ } catch { /* best-effort — no session settings, nothing to forward */ }
59
+
60
+ const coordinatorDaemonId = readNonEmptyString(settings.meshCoordinatorDaemonId);
61
+ const meshId = readNonEmptyString(settings.meshNodeFor);
62
+ // A non-delegated (non-mesh) session has no coordinator anchor — nothing to mirror.
63
+ if (!coordinatorDaemonId || !meshId) return 'skipped';
64
+ const nodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.meshLastNodeId);
65
+
66
+ // sessionSettings mirrors the exact keys updateMeshOwnedSession merges onto the mirror's
67
+ // settings (userHidden / userMuted). The coordinator's mirror resolver reads these to derive
68
+ // surfaceHidden/muted, so the dashboard sees the fresh value on the next metadata flush.
69
+ const payload: Record<string, unknown> = {
70
+ event: 'session:settings_changed',
71
+ meshId,
72
+ targetSessionId: sessionId,
73
+ ...(nodeId ? { nodeId } : {}),
74
+ sessionSettings: { ...patch },
75
+ };
76
+
77
+ // Await the mirror refresh (Fix 4). One retry: a transient P2P blip on the coordinator
78
+ // channel shouldn't strand the mirror stale when the worker's own state already moved.
79
+ for (let attempt = 0; attempt < 2; attempt++) {
80
+ try {
81
+ await dispatch(coordinatorDaemonId, 'mesh_forward_event', payload);
82
+ return 'refreshed';
83
+ } catch (e: any) {
84
+ if (attempt === 0) continue;
85
+ LOG.warn('Mesh', `[Mesh] set_conversation_prefs mirror forward to coordinator ${coordinatorDaemonId.slice(0, 12)} failed after retry: ${e?.message || e}`);
86
+ return 'failed';
87
+ }
88
+ }
89
+ return 'failed';
90
+ }
91
+
21
92
  export const cliAgentHandlers: Record<string, MedFamilyHandler> = {
22
93
  launch_cli: async (ctx: MedFamilyContext, args: any) => {
23
94
  // The coordinator routing anchor (meshCoordinatorDaemonId) is stamped
@@ -89,8 +160,25 @@ export const cliAgentHandlers: Record<string, MedFamilyHandler> = {
89
160
  // Push a fresh status snapshot so all clients see the updated
90
161
  // surfaceHidden/muted immediately (cloud path). The standalone server has a
91
162
  // parallel broadcast gate keyed on the command name (see daemon-standalone).
163
+ // Fired BEFORE the awaited mirror refresh so the local write is visible even if
164
+ // the coordinator mirror dispatch is slow or fails.
92
165
  ctx.deps.onStatusChange?.();
93
- return { success: true, sessionId, ...patch };
166
+ // MESH-WORKER-PREFS Fix B + Fix (4): if this is a mesh worker session, mirror the fresh
167
+ // prefs onto the coordinator so its stale meshOwnedSessions copy can't revert the toggle
168
+ // when it re-emits the dashboard metadata. AWAITED (with one retry) so a dropped mirror
169
+ // refresh is reported to the caller (mirrorStale) instead of silently leaving the
170
+ // coordinator to re-stamp the old value — the "restore does nothing" revert. No-op for
171
+ // non-mesh sessions. Never throws: the local write already succeeded.
172
+ const mirror = await forwardConversationPrefsToCoordinator(ctx, sessionId, patch);
173
+ return {
174
+ success: true,
175
+ sessionId,
176
+ ...patch,
177
+ // Only surface the mirror status for a genuine mesh worker session (refreshed/failed);
178
+ // a non-mesh session ('skipped') carries no mirror field, so nothing changes for it.
179
+ ...(mirror === 'failed' ? { mirrorStale: true } : {}),
180
+ ...(mirror === 'refreshed' ? { mirrorRefreshed: true } : {}),
181
+ };
94
182
  },
95
183
 
96
184
  agent_command: async (ctx: MedFamilyContext, args: any) => {