@adhdev/daemon-core 0.9.82-rc.370 → 0.9.82-rc.371

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.370",
3
+ "version": "0.9.82-rc.371",
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.370",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.371",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -19,6 +19,7 @@ import { meshCrudHandlers } from './mesh-crud.js';
19
19
  import { meshHostPairingHandlers } from './mesh-host-pairing.js';
20
20
  import { meshQueueHandlers } from './mesh-queue.js';
21
21
  import { fastForwardHandlers } from './fast-forward.js';
22
+ import { meshRestartHandlers } from './mesh-restart.js';
22
23
  import type { MedFamilyRegistry } from './types.js';
23
24
 
24
25
  export type { MedFamilyContext, MedFamilyHandler, MedFamilyRegistry } from './types.js';
@@ -31,5 +32,6 @@ export const medFamilyRegistry: MedFamilyRegistry = new Map(
31
32
  ...meshHostPairingHandlers,
32
33
  ...meshQueueHandlers,
33
34
  ...fastForwardHandlers,
35
+ ...meshRestartHandlers,
34
36
  }),
35
37
  );
@@ -0,0 +1,92 @@
1
+ /**
2
+ * RF-ROUTER MED family — coordinator-triggered daemon restart.
3
+ *
4
+ * restart_daemon_node exposes the existing dashboard "preview update" path
5
+ * (low-family daemon_upgrade: update-to-latest-on-channel + detached restart) as
6
+ * a mesh command, so a coordinator can roll a worker daemon onto a freshly
7
+ * deployed version without a manual restart round-trip. It mirrors
8
+ * fast_forward_mesh_node's remote-forward shape — resolve the target node, and
9
+ * if it belongs to a remote daemon forward the command there so the owning
10
+ * daemon (not the coordinator) restarts itself — and adds an idle-gate: a node
11
+ * with a generating / waiting_approval / starting session is refused so an
12
+ * in-flight turn is never killed mid-restart.
13
+ *
14
+ * v1 reuses daemon_upgrade verbatim rather than adding a restart-only path:
15
+ * the goal is "pick up a just-deployed version", which inherently needs the
16
+ * npm reinstall the upgrade helper already performs. Already-latest is a no-op
17
+ * (no restart), matching the dashboard button.
18
+ */
19
+ import { daemonIdsEquivalent, meshNodeIdMatches } from '@adhdev/mesh-shared';
20
+ import { daemonLifecycleHandlers } from '../low-family/daemon-lifecycle.js';
21
+ import type { CommandRouterResult } from '../router.js';
22
+ import type { MedFamilyContext, MedFamilyHandler } from './types.js';
23
+
24
+ // Session states that must block a restart: an in-flight turn or a pending
25
+ // approval would be lost when the daemon exits to re-spawn. Mirrors the
26
+ // daemon-cloud mandatory-update idle-gate (hasBlockingSessionsForMandatoryUpdate).
27
+ const RESTART_BLOCKING_STATES = new Set(['generating', 'waiting_approval', 'starting']);
28
+
29
+ function hasBlockingSessions(ctx: MedFamilyContext): boolean {
30
+ const states = ctx.deps.instanceManager.collectAllStates();
31
+ for (const state of states) {
32
+ if (RESTART_BLOCKING_STATES.has(String(state.status || ''))) return true;
33
+ const childStates = 'extensions' in state && Array.isArray((state as any).extensions)
34
+ ? (state as any).extensions
35
+ : [];
36
+ for (const child of childStates) {
37
+ if (RESTART_BLOCKING_STATES.has(String(child?.status || ''))) return true;
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ export const meshRestartHandlers: Record<string, MedFamilyHandler> = {
44
+ restart_daemon_node: async (ctx: MedFamilyContext, args: any): Promise<CommandRouterResult> => {
45
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
46
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
47
+
48
+ // Resolve the target node's owning daemon so a command that lands on a
49
+ // non-owner daemon is forwarded rather than restarting the wrong daemon.
50
+ // preferInline so inline-cache-only worktree nodes still resolve.
51
+ let nodeDaemonId: string | undefined;
52
+ if (meshId && nodeId) {
53
+ const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
54
+ const node = meshRecord?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
55
+ nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
56
+ }
57
+
58
+ const selfDaemonId = ctx.deps.statusInstanceId;
59
+ // daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's
60
+ // core is local — execute here instead of forwarding (and P2P self-dial).
61
+ // Equivalent → local. _meshDirectDispatch prevents re-forwarding once the
62
+ // call has landed on the owning daemon.
63
+ const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
64
+ if (isRemote && ctx.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
65
+ const forwarded = await ctx.deps.dispatchMeshCommand(nodeDaemonId!, 'restart_daemon_node', {
66
+ ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
67
+ _meshDirectDispatch: true,
68
+ });
69
+ return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
70
+ }
71
+
72
+ // Idle-gate: refuse if any session on THIS daemon is mid-turn / awaiting
73
+ // approval / starting. The coordinator restarts other (idle) nodes freely;
74
+ // restarting the coordinator's OWN daemon is naturally refused while its
75
+ // calling turn is 'generating' (accepted v1 limitation — call other nodes
76
+ // first, the coordinator last when it has gone idle).
77
+ if (hasBlockingSessions(ctx)) {
78
+ return {
79
+ success: false,
80
+ restarted: false,
81
+ code: 'blocking_sessions',
82
+ reason: 'Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle.',
83
+ };
84
+ }
85
+
86
+ // Reuse the battle-tested dashboard "preview update" path: update to the
87
+ // latest published version on the resolved channel, then detached-restart.
88
+ // Already-latest is a no-op (no restart), matching the dashboard button.
89
+ const result = await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
90
+ return { ...result, restarted: (result as any)?.restarting === true };
91
+ },
92
+ };
package/src/index.ts CHANGED
@@ -312,7 +312,7 @@ export type { CdpInitializerConfig } from './cdp/initializer.js';
312
312
  // ── Commands ──
313
313
  export { DaemonCommandHandler } from './commands/handler.js';
314
314
  export type { CommandResult, CommandContext } from './commands/handler.js';
315
- export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails, resolveMeshNodeAttribution } from './commands/router.js';
315
+ export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails, resolveMeshNodeAttribution, buildMeshNodeDataFreshness, MESH_NODE_LIVE_TRUTH_MARKER } from './commands/router.js';
316
316
  export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
317
317
  export {
318
318
  maybeRunDaemonUpgradeHelperFromEnv,